With the release of C++14, the standards committee strengthened one of the coolest modern features of C++: constexpr . Now, C++ developers can write constant expressions and force their evaluation at compile-time, rather than at every invocation by users. This results in faster execution, smaller executables and, surprisingly, safer code.

Undefined behavior has been the source of many security bugs, such as Linux kernel privilege escalation (CVE-2009-1897) and myriad poorly implemented integer overflow checks that are removed due to undefined behavior. The C++ standards committee decided that code marked constexpr cannot invoke undefined behavior when designing constexpr . For a comprehensive analysis, read Shafik Yaghmour’s fantastic blog post titled “Exploring Undefined Behavior Using Constexpr.”

I believe constexpr will evolve into a much safer subset of C++. We should embrace it wholeheartedly. To help, I created a libclang-based tool to mark as much code as possible as constexpr , called constexpr-everything. It automatically applies constexpr to conforming functions and variables.

Constexpr when confronted with undefined behavior

Recently in our internal Slack channel, a co-worker was trying to create an exploitable binary where the vulnerability was an uninitialized stack local, but he was fighting the compiler. It refused to generate the vulnerable code.

/* clang -o example example.cpp -O2 -std=gnu++14 \ -Wall -Wextra -Wshadow -Wconversion */ typedef void (*handler)(); void handler1(); void handler2(); void handler3(); handler handler_picker(int choice) { handler h; switch(choice) { case 1: h = handler1; break; case 2: h = handler2; break; case 3: h = handler3; break; } return h; }

When compiling the example code with a modern compiler (clang 8.0), the compiler silently eliminates the vulnerable cases. If the caller specifies a choice not handled by the switch (such as 0 or 4), the function returns handler2 . This is true on optimization levels greater than -O0 . Try it for yourself on Compiler Explorer!

My default set of warnings ( -Wall -Wextra -Wshadow -Wconversion ) doesn’t warn about this on clang at all (Try it). It prints a warning on gcc but only with optimizations enabled ( -O0 vs -O1 )!

Note: If you want to print all the warnings clang knows about, use -Weverything on clang when developing.

Periodic announcement: -Wall does *not* include all the warnings. Neither does -Wextra. Use -Weverything on clang but expect it to change, so don't pair it with -Werror — Ryan Stortz (@withzombies) February 21, 2019

The reason for this is, of course, undefined behavior. Since undefined behavior can’t exist, the compiler is free to make assumptions on the code — in this case assuming that handler h can never be uninitialized.

Right now the compiler silently accepts this bad code and just assumes we know what we’re doing. Ideally, it would error out. This is where constexpr saves us.

/* clang -o example example.cpp -O2 -std=gnu++14 \ -Wall -Wextra -Wshadow -Wconversion */ typedef void (*handler)(); void handler1(); void handler2(); void handler3(); constexpr handler handler_picker(int choice) { handler h; switch(choice) { case 1: h = handler1; break; case 2: h = handler2; break; case 3: h = handler3; break; } return h; }

# https://gcc.godbolt.org/z/gKrZV3 <source>:9:13: error: variables defined in a constexpr function must be initialized handler h; 1 error generated. Compiler returned: 1

constexpr forced an error here, which is what we want. It works on most forms of undefined behavior but there are still gaps in the compiler implementations.

constexpr everything!

After some digging in the clang source, I realized that I can use the same machinery libclang uses to determine if something can be constexpr during its semantic analysis to automatically mark functions and methods as constexpr . While this won’t detect more undefined behavior directly, it will help us mark as much code as possible as constexpr .

Initially I started writing a clang-tidy pass, but ran into trouble with the available APIs and the context available in the pass. I decided to create my own stand-alone tool: constexpr-everything . It is available on our GitHub and should work with recent libclang versions.

I wrote two visitors, one which tries to identify if a function can be marked as constexpr . This turned out to be fairly straightforward; I iterate over all the clang::FunctionDecl s in the current translation unit and ask if they can be evaluated in a constexpr context with clang::Sema::CheckConstexprFunctionDecl , clang::Sema::CheckConstexprFunctionBody , and clang::Sema::CheckConstexprParameterTypes . I skip over functions that are already constexpr or can’t be (like destructors or main ). When the analysis detects a function that can be constexpr but isn’t already, it issues a diagnostic and a FixIt:

$ ../../../build/constexpr-everything ../test02.cpp constexpr-everything/tests/02/test02.cpp:13:9: warning: function can be constexpr X(const int& val) : num(val) { constexpr constexpr-everything/tests/02/test02.cpp:17:9: warning: function can be constexpr X(const X& lVal) constexpr constexpr-everything/tests/02/test02.cpp:29:9: warning: function can be constexpr int getNum() const { return num; } constexpr 3 warnings generated.

FixIts can be automatically applied with the -fix command line option.

Trouble applying constexpr variables

We need to mark variables as constexpr in order to force evaluation of constexpr functions. Automatically applying constexpr to functions is easy. Doing so on variables is quite difficult. I had issues with variables that weren’t previously marked const getting marked const implicitly through the addition of constexpr .

After trying to apply constexpr as widely as possible and fighting with my test cases, I switched tactics and went with a much more conservative approach: only mark variables that are already const -qualified and have constexpr initializers or constructors.

$ ../../../build/constexpr-everything ../test02.cpp -fix constexpr-everything/tests/02/test02.cpp:47:5: warning: variable can be constexpr const X x3(400);</code> constexpr constexpr-everything/tests/02/test02.cpp:47:5: note: FIX-IT applied suggested code changes 1 warnings generated.

While this approach won’t apply constexpr in every case possible, it can safely apply it automatically.

Try it on your code base

Benchmark your tests before and after running constexpr-everything . Not only will your code be faster and smaller, it’ll be safer. Code marked constexpr can’t bitrot as easily.

constexpr-everything is still a prototype – it has a couple of rough edges left. The biggest issue is FixIts only apply to the source ( .cpp ) files and not to their associated header files. Additionally, constexpr-everything can only mark existing constexpr -compatible functions as constexpr . We’re working on using the machinery provided to identify functions that can’t be marked due to undefined behavior.

The code is available on our GitHub. To try it yourself, you’ll need cmake , llvm and libclang . Try it out and let us know how it works for your project.

Share this: Twitter

LinkedIn

Reddit

Telegram

Facebook

Pocket

Email

Print

