Making C++ Safer

Undo Bytes
6 min readFeb 27, 2025

--

Would you start a new project in C++ these days? Many people would say no, they’d use something else; usually they cite Rust. Or Go, or Python or anything “memory safe”, which these days effectively means anything that isn’t C or C++.

A few years ago this kind of discussion was for language nerds hanging out on Hacker News; now it’s about geopolitics! States are working ever harder to hack each others’ systems, and the majority of critical infrastructure code is written in C++ (or plain C), and this can be a gift to hackers. The US government is now urging programmers to drop C and C++. There were some very misleading headlines recently, saying “Whitehouse mandates companies stop using C++ by 2026”. Of course, they didn’t say this, because it would be nuts — they actually strongly encouraged software companies to produce a memory safety roadmap by 2026. The argument around should we use C++ or not is moot — there are an estimated 10 billion lines of C++ code in production today; rewriting it all will take decades.

I believe that over time C++ will become a lot safer, maybe even some kind of ‘safe’. Competition is good: Clang was the best thing to happen to GCC, and Rust might turn out to be the best thing to happen to C++. That journey has already begun, with proposals for the evolution of the language including Contracts and Profiles, and simply changing some of the defaults in C++26. While the language custodians work to make the language itself safer, what can you do today?

Follow the Core Guidelines

Even today, most C++ doesn’t need to be nearly as unsafe as it is. If everyone had followed the existing Core Guidelines, we’d already be in much better shape. The guidelines are a set of simple rules which, if followed, make C++ a lot safer, as well as easier to read and reason about, and more consistent. They are maintained by Bjarne Stroustrup and Herb Sutter, and they cover issues such as interfaces, resource and memory management, and concurrency. For example: RAII (Resource Allocation Is Initialization) can be used to make many simple resource safety mistakes, such as a leak or use-after-free, no longer possible. When writing or reviewing new code, or changing existing code, follow them. As discussed however, we can’t just rewrite all that code, so how do we live with all that existing code that doesn’t follow the guidelines?

Bounds checking by default

There is simply no excuse for many (most?) of the safety violations we see today: stupid out-of-bounds errors accessing arrays, vectors, strings, etc. Google recently published a blog where they report enabling bounds checking and other hardening resulted in a performance impact of just 0.3%. Just enable such bounds checking by default. See here for how to do so with Clang/LLVM, and here for GCC. If you determine by profiling that some performance-critical piece of code is being materially slowed down by this, you can always disable the bounds checking for that one bit of problematic code. (My bet is that you won’t though.) Chances are that C++26 will enable such bounds checking by default anyway, but why wait?

Stop ignoring those flaky tests

Those flaky test failures are telling you something, whether or not you’re writing in a memory safe language, but with memory unsafe languages a good proportion of them will be memory errors. This is actually part of something bigger — memory safety is just one kind of vulnerability, and the same concerns that are driving the memory safety debate are affected by undiagnosed, flaky tests.

Fixing a flaky-test problem takes commitment, but it’s well worth it. Engineers from Google have written a lot on it, as we at Undo published a handy 7-step guide.

Use the tools: sanitizers

Use the sanitizers, particularly ThreadSanitizer, AddressSanitizer, and Undefined Behavior Sanitizer. Your CI should run a complete run of the tests with sanitizers enabled. While this won’t catch all memory safety problems, it will catch many of them. Hunt down all the failures, be very wary of dismissing them as false positives. (See flaky tests above.)

They’re super easy to use these days — you just need to pass the option -fsanitize=address, -fsanitize=thread or -fsanitize=undefined when you compile (either gcc or clang), and that’s it. There are lots of options you can tweak, but to get started, the defaults work fine. See this article for more details.

Use the tools: static analyzers

Many of the memory safety bugs that lead to security vulnerabilities can be identified by modern static analysis. Even the best static analyzers suffer from generating false positives, and if you haven’t run them before the list of warnings can be daunting. The good ones will help you prioritize which ones to look into first and/or can be configured to only show you issues in new/changed code. Static analysis is one of those tooling categories where if you want quality you need to pay for it — the cost of developing and maintaining them mean it’s difficult for traditional open source models to work. Coverity, Klocwork and SonarQube are popular options. If you want to start with open source, you could try cppcheck.

Use the tools: fuzzing

Fuzzing typically uncovers all kinds of edge cases you didn’t think about. Many of them will be safety concerns. Like static analysis, you can get a bewildering number of failures, but if you run using fuzzing and time travel debugging it is usually pretty trivial to root cause and fix the issue when you can step back through a recording.

There are good free and commercial offerings. For example, Google’s FuzzTest is free, flexible and fairly easy to use. It will find bugs in your code that you didn’t know about. It is built on top of AddressSanitizer (see above). On the commercial side, BlackDuck’s Defensics is very good, as is Code Intelligence’s CI-Fuzz.

Stop papering over the cracks

When you smell smoke, you act. If a test is failing or a bug is reported and a small change makes it work but you don’t understand why, spend the time to do the root-cause analysis. When your code is behaving in a way you don’t understand, it’s telling you something. If you don’t keep pulling on that thread to properly understand it, chances are that you are leaving a safety vulnerability that may later be discovered by malign actors — or maybe it already has been!

Again, a time travel debugger can make this much easier.

Greater than the sum of its parts

Finally, do all of the above together. Sanitizers and fuzzing are a powerful combination, and when combined with time travel debugging you’d be surprised at how quickly you can squash all of the problems you find, one by one. When fuzzing throws up an error sometimes the root cause is obvious (you forgot to sanitize that user input), but other times the reason for the bad behavior is anything but obvious — combining fuzzing with sanitizers can really help narrow it down, and/or feed the fuzz failure into a time travel recording.

Conclusion

Buggy code is unsafe. Code we don’t understand is unsafe. Memory unsafe languages like C++ make us especially vulnerable, so when using them we must compensate. However, it is important to remember that memory safe languages are no panacea. If you use good software engineering practices and make best use of the tooling available, you will produce safer, better code, whatever language you’re using. In fact, a C++ codebase managed by a team that does all this is likely to be safer than the equivalent Rust by a team that ignores it. And not just safer, but also more pleasant and productive to work on.

Oh, one last thing: AI-generated code

No software article these days is complete without a reference to AI-generated code. AI can be a boon for developer productivity. But without care, it can be a nightmare for safety. Lots of AI-generated code without good practices and tooling threatens to be a recipe for disaster. Particularly if you’re writing systems code, then it is incumbent on you fully to understand the code written by you, or the AI on your behalf. Just staring at it and then accepting is not enough — you must use thorough testing, fuzzing, sanitizers, and most importantly make sure that you fully understand what the code is really doing.

I’d love to know your thoughts on the topic of C++ safety. Please connect and message me on LinkedIn to let me know.

--

--

Undo Bytes
Undo Bytes

Written by Undo Bytes

Undo is the time travel debugging company for Linux. We equip developers with the technology to understand complex code and fix bugs faster. https://undo.io

No responses yet