Another 0-day security bug in Chrome has been found being used “in the wild”. Once again, it was built on the bug class of integer overflow. This is not the first integer overflow used in an 0-day against Chrome, even within this year.
When a security bug is used in the wild, it means that it is being used to attack users of that software and that the vendor found out about it. These attacks are going on all the time, with no real way to actually know what bugs exist and are being used like that until you find one being used. The ones that go undetected can remain in active use indefinitely, until the software vendor makes systemic changes to make those types of bugs go away.
Software vendors never talk about how a 0-day security bug is being used, so news media doesn’t have much to go on and could only really speculate. However these bugs are worth millions of dollars. They can be used for things that hurt people and society, like espionage and intelligence, war, assassination, theft and ransomware, oppression, surveillance, and supporting human rights abuses. And when a piece of software becomes ubiquitous, the reach of attacks in that software grow, as should the responsibility to stop them from happening.
Integer overflow in C++
Chrome is written in C and C++ (with a tiny touch of Rust now), and thus is highly vulnerable to the classes of security bugs that affect these languages. Integers in C and C++ follow some very problematic rules that cause these types of bugs:
- They will implicitly convert to smaller types, truncating values and changing the valid range.
- They will implicitly change sign, changing the meaning of their value and their valid range.
- Unsigned integers will implicitly wrap around on overflow.
- Signed integers will cause Undefined Behaviour (essentially, mis-compilation) on overflow.
- Additionally, converting from floating point values to integers can also cause Undefined Behaviour.
Integer overflow is a common 0-day target in C and C++ codebases. Developers don’t write software expecting integers to overflow. So when they do, the program gets into an unexpected state, and can become a useful weird machine for an attacker.
Every major C or C++ codebase that is ubiquitous across user devices, and interacts with remote data like images or websites will be similarly valuable to attackers, and vulnerable to the same classes of bugs. And we see active 0-day bugs being reported in such software products regularly (for example, iOS and Android, Safari and Firefox ).
The safety of C++ is a hot topic right now, with CISA calling out languages that lack the ability to prevent these types of security bugs, specifically C and C++, and advocating for the use of memory-safe languages.
This has led to a bit of a reckoning in the C++ language design community. The CppNow 2023 conference had multiple talks on C++ Successor Languages, which positions moving off C++ as the strategy to stop putting customers and users into harm’s way.
- Carbon: Carbon Language Successor Strategy: From C++ Interop to Memory Safety - Chandler Carruth
- Swift: Introducing a Memory-Safe Successor Language in Large C++ Code Bases - John McCall
- Cpp2: The Evolution of C++ - A Typescript for C++ - Herb Sutter
- This is the least “moving off C++” of the three, but still entails working in a new syntax/language even if it shares a compiler.
There’s also been many talks on making C++ itself safer at CppNow, CppCon, and elsewhere. For example:
- Safety and Security: The Future of C++ - JF Bastien
- Timur Doumler: C++ and Safety
- Coding for Safety, Security, and Sustainability in C++ - Panel Discussion
- All the Safeties: Safety in C++ - Sean Parent
- Bjarne Stroustrup :: Approaching C++ Safety
Most recently, Dr. Stroustrup gave the Delivering Safe C++ talk at CppCon 2023.
The first challenge for the C++ language design community has seemed to be the task of figuring out what “safety” means. The vast majority of software exploits are written against memory safety bugs, and CISA has been clear that memory safety is the issue they are concerned with. I think the C++ community is slowly coming to focus on this as well, though its leaders do still want to consider a broader scope of safety for better or for worse.
In “Delivering Safe C++”, Dr. Stroustrup talks about profiles to address some forms of safety, and one of those is arithmetic. This to the class of bugs with integer and floating point types, like integer overflow, for which C++ currently lacks the guardrails to protect developers and their customers alike.
Integer overflow elsewhere
There’s two other important languages that C++ developers should be thinking about for production code right now: Swift and Rust.
John McCall talks about Swift’s position on integer overflow in this video from CppNow. The language does not allow integer overflow. The designers consider an overflowing value to be a bug in the program which leads to the program misbehaving, and they consider it better to stop the program than to let it continue running in a state it was not designed for. This is completely in line with what you’d want to protect your software and your device from being exploited.
Rust is known for being memory-safe yet only traps on integer overflow in debug builds,
by default. Chrome enables overflow trapping in Rust for release builds too, for now, but
how does Rust get away without catching overflow in release builds by default? I believe it
is primarily because Rust makes pretty much all pointer access go through slices, which
include bounds checks. There are no memcpy
, memset
, malloc
, or alloca
functions that
take a pointer without a bound. You use Vec
for memory allocation and slices provide
safe APIs for copying contiguous ranges of data around. It is still sketchy to allow overflow
to go unchecked however, and definitely leaves your program in an unexpected state which
has the potential to be abused. However it’s critical to note that Rust defines the behaviour of
overflow in release builds even if it’s allowed; there is no Undefined Behaviour with signed
integers as we see in C++.
What about upcoming potential languages?
So far, Carbon takes the same position as C++ for release builds, with Undefined Behaviour on signed overflow, and no trapping on overflow in general. It’s worth noting the plan includes trapping in debug builds, like with Rust. Though the high level of interop with C/C++ may leave the language with a similar level of vulnerability as C++, depending on how code ends up being ported to or written in Carbon.
Zig makes overflow into Undefined Behaviour in release builds and panic/trap in debug builds. They provide wrapping arithmetic operators to make it explicit and lightweight, but the risk of Undefined Behaviour in release builds would leave the language vulnerable to the same class of bugs.
C has all the same problems as C++ but fewer ways to address them, since it does not allow creating abstractions with the same power as in C++. C has recently gained library functions for performing checked arithmetic. But opting in explicitly to safety means the defaults will still do the dangerous thing and bugs will persist.
Solving integer overflow (and the class of arithmetic bugs) in C++
C++ gives us the opportunity to build abstractions that change or redefine behaviour of
the code we write. A great example of such an abstraction in the memory-safety space is
raw_ptr<T>
, aka
MiraclePtr. This is
an abstraction around a native pointer that works with the allocator to keep the memory from being
reclaimed and reused while there’s a dangling pointer to it.
Over 2022 and 2023 I have been building an abstraction for numeric types that drop in as a
replacement for primitive types (like int
, float
, uint32_t
, etc.) and which eliminate
the class of arithmetic unsafety bugs in C++.
These new types are a drop-in replacement as they implicitly convert to and from primitive integer types, when there is no truncation or sign change required. This allows them to be used while working with APIs built on primitive types. And it allows the migration of APIs to use them even while callers continue to use primitive types. However, they will catch at compile time any callers that were previously losing potential data in primitive types.
These types protect against integer overflow by panicking in the default operations (+
, -
,
*
, /
, %
). But since they are a user-defined type, they can provide a rich API for handling
overflow dynamically, saturating, or wrapping. And they can give you Undefined Behaviour on signed
overflow if you really want it, but you have to ask for it in an explicit way that both you and
your code reviewer can see.
Read more about the types in the API documentation here: https://suslib.cc/sus-namespace.num.html.
For basic stuff, they work just like primitive types. i32
takes the place of int
in
most cases. And they work with streams or fmtlib out of
the box.
void fib(i32 n) {
if (n < 3) return;
i32 fn = prev1 + prev2;
prev2 = prev1;
prev1 = fn;
std::cerr << fn << " ";
fmt::print("{} ", fn);
return fib(n - 1);
}
Designing is expensive and risky, and creating new APIs for folks to learn is a problem. We have enough APIs! So rather than make something completely new, the numeric APIs are modeled after Rust’s numeric APIs.
std::optional<usize> calculate_size(usize elem_size, usize length, usize offset) {
auto [acc1, oflo1] = elem_size.overflowing_mul(length);
auto [acc, oflo2] = acc1.overflowing_add(offset);
if (oflo1 | oflo2) {
return std::nullopt;
} else {
return acc;
}
}
If you are familiar with Rust, then you already know the provided APIs, there’s nothing more to learn. If you’re working in a mixed-language codebase, you can use the same APIs and expect the same behaviour regardless of where you happen to be writing code that day.
Making overflow checks optional
I really want to stop seeing integer overflow bugs in the news, and for me that means catching overflow as it happens in production, as integers in Swift will do.
However applying these checks to existing code can be risky:
- There are unknown stability risks. What if there are load-bearing overflows that you were not aware of? Maybe they are a bug that you would fix, once you know about it, but crashing the program would be catastrophic. Or maybe you want to preserve the overflows and apply wrapping behaviour.
- There are unknown performance risks. Using safe numeric types with overflow checks does imply some performance costs, and without the ability to turn them on and off, it’s impossible to verify what the cost is and do the performance work needed to drive those costs down through different algorithms (such as iterators instead of indexing) or with explicit unchecked arithmetic.
With the Make overflow checks optional at compile time PR that landed today, the overflow checks in these types are now able to be turned on and off at compile time. Crucially, like Rust, when disabled the overflow behaviour of signed integers is still well-defined, the conversions between integers and floating point types are all well-defined, uninitialized memory is always avoided, and invalid type conversions are still prevented at compile time.
These safe numeric types correspond to the overflows and unanticipated conversions safety profile proposed by Dr. Stroustrup, where different types of codebases can opt into different behaviour. While a piece of software that exposes you to attackers on the internet (like a browser, or a phone OS) should do everything possible to keep you safe, there are classes of software that do not have to deal with security threats. Yet even there, when debugging, the ability to turn on runtime checks can save many hours.
By using a new set of types, instead of a compiler warning, it is possible to apply safer arithmetic and numeric type safety across an existing codebase in an incremental manner. By making overflow checks optional, it is possible to look for overflow in fuzzers and tests and work toward preventing overflow in production in an incremental manner.
There’s still some work to do on these types and their associated types in the library, and a sharp edge in the language to work through, but they are already proving useful in my experience. The type safety prevents doing the wrong thing by accident, and the overflow checks make debugging any surprises almost trivial, instead of the gnarly debugging sessions that C++ normally makes me think of. I am intending to reach a public release of them very soon.