I'm glad MTE is catching on, I think it's the future for C and C++'s memory safety story. Hopefully intel comes out with something for it soon too!
Vale is taking a similar approach at the language level, by putting a generation at the top of every allocation [0] and it's proving pretty fast. It's also proving more accurate, since its generations are larger; MTE uses 4-8 bits, and generational references use 32-64.
It would be cool to see C++ itself embrace generational references in the standard library (perhaps via a std::gen_ptr?), it would give us some memory safety without needing to use the heap, which would be a big improvement over modern C++'s encouragement of std::shared_ptr.
Confused, why are generational references being compared with reference counting? One is intended for shared ownership, the other is intended for memory safety. They seem completely orthogonal/unrelated to each other; they look like complements rather than competitors.
I'd also be interested in knowing whether they compared against a single-threaded reference counter or a multi-threaded reference counter. The atomics in a thread-safe RC severely impact performance; I hope they didn't compare that against GR.
I see RC and GR as two methods of ensuring memory safety: one that extends the life of an object, and one that asserts the object is still alive. They both protect us from vulnerabilities and UB, but with different approaches.
Yep, it was compared to single-threaded RC for fairness.
RC isn't there to protect you from vulnerabilities though. It's there to let you share ownership. There's some side benefits in that regard, but the latter can't substitute for the former except in cases where the former is unnecessary to begin with.
As an analogy, imagine comparing a safety knife, a kitchen knife, and (a safe version of?) an axe with each other, and touting how much lighter/safer/whatever the first one is compared to the others. Of course that's correct, and you can make the comparison, but there isn't (or at least shouldn't be?) a huge market of people out there using an axe in the kitchen who could've just used a knife instead, so it's a weird choice of a market to sell an otherwise-great product to.
(And thanks for clarifying the single-threaded aspect.)
And ownership - shared or otherwise - is a great way to eliminate the use-after-free bugs that would otherwise be caused by naively attempting to create a non-owning, borrowing reference to something else. It is in fact the primary use of refcounted pointers in many C++ codebases I've worked with - perhaps with a single std::shared_ptr and several std::weak_ptr s, meaning you're not really sharing ownership per se despite the refcounted nature - or perhaps with multiple std::shared_ptr s out of sheer laziness.
I've had my share of C++ code reviews dinged for perfectly valid code containing non-owning references, simply because such code could be fragile and prone to use-after-free heisenbugs in the face of refactoring, and my reviewers were right to do so. Things are safer in Rust - you can play fast and loose with storing &str s and rely on the borrow checker to catch your mistakes - but playing fast and loose with storing std::string_view s will lead to slipped release dates as you chase 11th hour heisenbugs. Store a std::string or std::shared_ptr<std::string> instead, even if it's "unnecessary".
> there isn't (or at least shouldn't be?) a huge market of people out there using an axe in the kitchen who could've just used a knife instead
Some of us are out camping in the wilderness that is C++ gamedev, and want to make kindling. Do I need to pack a whole axe, or would a knife I need for other reasons anyways make do and lighten my backpack?
And even if that's a small market, it's still entirely a reasonable market, and for advertizing to that audience - however niche - comparing said tools would be an entirely justifyable approach to selling the product.
I also think you underestimate the size of said market.
> It is in fact the primary use of refcounted pointers in many C++ codebases I've worked with - perhaps with a single std::shared_ptr and several std::weak_ptr s [...] Store a std::string or std::shared_ptr<std::string> instead, even if it's "unnecessary".
Uh, I've been wondering at the use case for rc::Weak, that seems like an interesting halfway point if you think borrowing should work but you can't get the compiler to accept it.
shared_ptr is just a code smell.
There aren't really many good use cases for shared ownership, using it is just a cop-out because you can't design code properly.
Even if true, that's overly dismissive and unactionable, especially when several programming languages bake the same reference counting into their most basic reference semantics, as a poor man's half baked non-sweeping garbage collector. Further - find me a programmer who always designs code properly, and you will have found me a liar.
One actionable alternative is explicit owner/borrowing smart pointers, which assert if an owner is destroyed while borrowers are pending - which while not eliminating bugs, at least makes them much shallower to debug. I don't have hands on experience with that style - I inherit codebases and their existing coping patterns too often - but I hear good things from those who do. Another actionable alternative is resorting to a proper garbage collector. Or using a proper COM pointer type. Or RIIR. I resort to Rc/Arc in Rust much less than I resort to shared_ptr in C++ (although it still has its niches.)
`Common` (shared between request-handling worker threads) could be made static instead of Arc (although this would eliminate having separate isolated state for, say, different hosts?) Or I could use a scoped thread API that ensures the worker threads are cleaned up before the main thread does, and leave it on the stack instead. The standard library's scoped threads are nightly-only, but there are third party crates - or I could roll my own with `unsafe { ... }`.
`Arc<String>` messages (shared between chat history broadcasting threads until every recipient has sent out a reply) could be simply deep copied. Given the small typical message sizes, that might even be cheaper (although with the heap allocs I wouldn't bet on it.) Or a proper thread safe mpmc FIFO container could be used, although none is in the standard library.
A 200 LOC monolithic main.rs isn't exactly well designed, but it didn't need to be. That's a cop-out, but I suspect your pull requests won't be forthcoming either ;).
C++ game dev is certainly fun. I feel like the push to more managed languages in engines is making it a smaller and smaller field, as people are transitioning to C# with frequency.
You can certainly implement generational pointers in C++ code without further language-level support (given that you’re using your own handmade object pools). Don’t know if this can get to the STL though, since it requires a malloc (or object pool) implementation tailor-made for it.
I’ve implemented this in my current C++ game engine, and works wondrously well. You can even pack that generational index in the unused 16-bits of a 64-bit pointer, if you have further constraints (like a C library only allowing void* as userdata)!
> they are halfway to implementing a garbage collector already
This is a common misunderstanding.
The key insight of this work is actually "morphing dangling pointers into memory leaks is WAY EASIER than automatic memory management (a.l.a. GC or refcount)". As long as we still do memory management by hand, we are able to avoid most of the complexity of GC (or defects of RC) and convert one bug class to another without much overhead.
Yes, this work can't eliminate bugs in your code (unlike, say, Rust, or other GC languages). But some bugs matter more than others, and, it works on existing code, it makes significant contribution to security without rewriting the world with Rust or JavaScript.
Yeah, that's the thing, there are some domains that aren't going to ever move away from C and C++ dynamic duo no matter what, at least until something like quantum takes over (how many decades still?), so any help to improve their security is welcomed.
> aren't going to ever move away from C and C++ dynamic duo no matter what
Forever is a long time.
> any help to improve their security is welcomed.
I'm less certain of this than you are, after decades of watching "We may not have really solved the problem, but this helps 99%" almost immediately followed by "Oh, our adversaries apparently aren't idiots and so our 99% mitigation did not actually reduce our burden significantly".
For domains where you don't have security considerations, this is fine. If you make single player video games a mitigation which means you have 4 crash bugs to fix in your game instead of 400 is excellent news. The bugs aren't trying to catch you, so mitigations are effective. But in security your attackers will put no effort into cutting through your expensive thick steel reinforced concrete wall, and focus their effort instead on picking the $4 padlock on your gate in that wall or vaulting over the wall, or just say they're a package delivery and drive in past your guards or a dozen other things you never thought of.
Until stuff like CUDA, DirectX, LibGNM, LLVM, GCC, mainstream language runtimes, UNIX clones, IBM mainframes, Windows, Machine Learning, HPC stuff like MPI and OpenAAC, move away from C and C++, I will stay with my forever statement.
Your game development example, other than Embark Studios, very few care about.
Not to mention that Unreal, Unity, Godot are the bare minimum to beat.
AAA game companies that are security conscious are already using .NET, Java, Go on their game servers.
> Until [...] I will stay with my forever statement.
That's not how forever works.
As to the specifics, there's some sign that things like the GPU APIs are realising they did care about memory safety after all - when the problem caused by a memory safety bug is that some particle effects look a bit wonky every tenth frame in a game and you find zeroing the Z buffer fixes it even though it logically shouldn't that's never getting off the backlog, but when it causes your $10M compute cluster to spit out bogus results unless you reboot it between runs that's $$$ wasted.
I fully expect that for a while they will be enthusiastic about all the other things that attracted people who eventually end up with Rust - they'll purchase static checkers, they'll write code in a different style, they'll invest in more sophisticated debugging tools, but there's a reason Rust happened.
For the compilers, well, I'm old, twenty years ago I was a full-time C programmer, today nobody would do what we did in C back then, maybe they'd try in C++? But the thing which initially attracted people to C++ was that whole "Won't do better without hand-rolled machine code" and that has rotted pretty badly even in twenty years because of... drum roll, ABI compatibility.
The compiler vendors care about performance, long after apparently many C++ programmers are content to produce code with similar performance to Python for ten times the cost and a fraction of the safety, the compiler vendors are still under pressure to ship stuff that has decent performance but also correctness. Rust looks like a better fit for that every year.
well you don't actually have to allocate memory to keep it blocked do you? you can just have a page that isn't present and isn't actually backed by physical memory
It is possible to allocate every object in its own page. In practice this is useful as a debugging tool but far too expensive for day-to-day operation.
Maybe the real issue to solve is to reduce the page size to make this a feasible approach to mitigation. Hardware designers have made this choice, but, does it really need to be 4096 bytes?
With a much smaller size, say, 128 bytes, almost every new allocation could be put into a new page at a new address, with one or more guard pages behind it.
Yes, it would increase the size of the page tables, and the lookup time, but maybe it's worth it.
The example in the article is something that, for the past decade or so, has been discouraged more and more strongly and is almost entirely unnecessary: Manually allocating and freeing memory (plus letting a variable outlive its use and be used for something else).
to the question of why C++ doesn't have a garbage collector.
To be more concrete, the example would be written today as:
{
auto foo = std::make_unique<Foo>();
}
foo->Process();
the last line won't compile - foo is gone already; and if you moved it inside the block scope, then foo would be valid.
Now, the guidelines I mentioned can also be partially statically-checked, and some IDEs have begun to do so, so that the original snippet would give you at least 3 or 4 warnings. Interestingly, this will be true in a larger codebase as well, despite the claim in the article that
> UAFs are often hard to spot in larger codebases where ownership of objects is transferred between various components.
and that's because if you adhere to the coding guidelines _locally_, but throughout your application, including also I.11 about ownership transfer, then you can never free a pointer you don't own (unless you explicitly decide to do so), and you can even avoid dereferencing null pointers. Yes, I meant what I said: You can simply avoid ever dereferencing a null pointer.
All of this is supported by static analysis, so you absolutely don't need to be a virtuous and skilled coder to avoid such conundrums.
---
There's a caveat though: If some of the code is not under your control, but is rather some 3rd-party library or header, then what I said only applies if you properly "sanitize" at the boundaries of your well-checked.
The shared_ptr is an optional garbage collection based on reference counting. Slow, leaky, deterministic (that's an advantage and a disadvantage). C++ needs optional garbage collection if needs shared_ptr.
C++11 introduced APIs for optional GC and since no one has ever fully provided one, it is going away on C++23.
The way C++/CLI, C++/CX and Unreal C++ work isn't based on those APIs (naturally, given their domain), and are even used on the C++23 motivation paper as why those GC APIs should be removed.
ITT everyone latching on to the simplistic UAF example, which is obviously not how actual UAFs happen in Chrome or most other production C++ codebases.
The article specifically mentions smart pointers and their limitations.
The article describes what it claims to be limited mitigation efforts; but what I commented about is how they make the problem go away, if you actually use them. If you have some smart pointers but also allocate and deallocate memory manually in all sorts of places in your app, and transfer ownership via raw pointers, then - yeah, you might have UAF problems. In 2011 you could have blamed the lack of language features and library facilities to avoid this; but those days are over. UAF problems are now pitfalls you have to actively and knowingly walk into, they don't just happen.
There are other issues with safety which still happen, like out-of-bounds access, which don't have this kind of "effective inoculation"; so it's not as though C++ is now a completely safe language. But UAFs are now mostly a non-issue.
So in other words, UAFs are a non-issue except in every actually-existing large production C++ codebase in the world. Thus no point in investigating memory safety techniques?
Chrome ships with half a dozen pointers that are "smarter" than the code you mentioned. There are still issues because of thread safety and needing to extract dangling pointers at times.
I find it quite discouraging to see how they basically give up trying to construct code without security flaws, and instead hope for some magic hardware mitigation to rescue them.
Investing in mitigations is not free. The time and effort it takes to develop them is not available for constructing bug-free code. And once you have them, there will be even less reason to construct bug-free code. After all the magic hardware-assisted mitigations will save us now.
We've been through this before multiple times now. Someone will find a way to exploit around your mitigation, and you will then double down and come up with the next mitigation that will also be circumvented eventually.
Personally I believe that "retrofitting" security can never work well. Security must be constructed, not retrofitted. Why is this example code using raw allocations? Construct secure code by using containers, not manual allocations. This has been the recommendation for years now.
Google C++ guidelines is well know among the community for not being the best in what concerns C++ good practices, only C++ outsiders look at it as something of value.
Something like C++ Core Guidelines, which include your remarks, is what should be looked into.
Naturally Chrome follows Google's C++ guidelines, so.
right, it is nitworthy that Google guidlines are soecific to a code base within an organisation. There is quite some value in reading the rules and reasoning, but it isn't Gospel by any means.
Google's C++ guidelines have almost nothing to do with Chrome's memory safety efforts. The concerns brought up in the article are relevant to other browsers too.
I guess the authors used a raw allocation in the example code to show the simplest possible use-after-free.
Any software with complex object ownership written in a language without GC has to balance between user-after-free (freeing an object too early) and memory leaks (keeping an object alive longer than needed). Maybe constructing secure code here would mean simplifying object ownership, but that doesn't seem practical for a beast such a modern browser with millions of lines of code.
MTE with the heap scanning sounds like a real solution for the user-after-free problem, not merely a mitigation. The trade-off is that the memory is not immediately freed, but the measured increase of only 1% is promising.
- you have some AST object and want to lower it in the compiler into an intermediate representation
- you want to add some interesting parts of the AST to your state while lowering and refer to them later in the lowering process
- This seems fine: the AST is first created, then you lower to the intermediate representation, then you can destroy the AST, so while you’re doing the lowering, the AST objects should all be alive and therefore ok to store references to in the lowerer-state
- However there is some mostly unnoticed code that is roughly desugaring some syntax by constructing temporary AST nodes
- So your addition may have your state including references to these temporary nodes
- And those temporary nodes are freed shortly after being created rather than after the lowering is all done
Constructing bug-free code is infeasible. Even the most skilled programmers write code with bugs in, and most programmers, including many who work on browsers components, are only moderately skilled, often writing buggy code.
Perhaps the longer-term solution is to use languages that are memory safe by design, such as Rust, to avoid that class of bugs. But there will still be a huge deal of legacy C++ code to contend with in the meantime, so we do still need exploit mitigations.
Clearly you can't expect programmers to just write secure code. If that worked, we would have seen evidence for it working by now.
By construct I mean: follow a clear method or path, that is a) feasible to follow and b) will lead to bug-free code. Maybe not bug free in all respects but I posit it should be possible to construct code without memory safety issues (just look at Perl or Rust).
My point is: It should not be up to the programmer to "simply not make mistakes". The method should be clear and have little ambiguity, and it should be obvious to see if code actually follows it or not.
I can't be more concrete or specific than that because I think we still need to find that method. We should be working on it, though.
> Investing in mitigations is not free. The time and effort it takes to develop them is not available for constructing bug-free code.
I feel like this is a bit of a false dichotomy. Writing bug-free code is one approach to getting bug-free software. Attempting to detect UAF at run time is another approach. And there are many other options beyond those two (static analysis, sanitizers, testing, ...).
In theory, we need only the ability to write bug-free code. If we could do this reliably, repeatedly, in a reasonable amount of time and at a large scale (Chromium, in this case), then there would be no value in investing in any of these other approaches.
In practice, fixing every last bug in a large codebase can take a truly immense amount of time. And that is for a codebase that is not changing at all (including bug fixes!), which certainly does not describe Chromium or most large projects for that matter.
Given all that, it's entirely reasonable to think about how one can find the most bugs with the least amount of effort. And that's basically what this is--another form of automated bug detection. I really don't think it's intended as a substitute for correct code.
I'm not advocating for "fixing all the bugs". On the contrary.
I argue that retrofitting security does not work.
I argued that we need to find a way to construct secure software, not to make existing insecure software more secure retroactively. We have tried writing shitty software first and then try to make it more secure for decades now. The track record is not good. We should try something else. Like constructing secure software from the start.
Many decades of work has demonstrated fairly clearly that it is not feasible to simply demand that people write secure code. "Just write it correctly" also does not help people who are starting at giant piles of C++ code that is a major security risk but also don't want to rewrite from scratch.
51 comments
[ 3.3 ms ] story [ 124 ms ] threadInterestingly, the mention MTE. Could that be used in GC as well? To in place mark the generation of an object without having to copy it maybe?
> MTE (Memory Tagging Extension) is a new extension on the ARM v8.5A architecture that helps with detecting errors in software memory use.
Vale is taking a similar approach at the language level, by putting a generation at the top of every allocation [0] and it's proving pretty fast. It's also proving more accurate, since its generations are larger; MTE uses 4-8 bits, and generational references use 32-64.
It would be cool to see C++ itself embrace generational references in the standard library (perhaps via a std::gen_ptr?), it would give us some memory safety without needing to use the heap, which would be a big improvement over modern C++'s encouragement of std::shared_ptr.
[0] https://verdagon.dev/blog/generational-references
I'd also be interested in knowing whether they compared against a single-threaded reference counter or a multi-threaded reference counter. The atomics in a thread-safe RC severely impact performance; I hope they didn't compare that against GR.
Yep, it was compared to single-threaded RC for fairness.
As an analogy, imagine comparing a safety knife, a kitchen knife, and (a safe version of?) an axe with each other, and touting how much lighter/safer/whatever the first one is compared to the others. Of course that's correct, and you can make the comparison, but there isn't (or at least shouldn't be?) a huge market of people out there using an axe in the kitchen who could've just used a knife instead, so it's a weird choice of a market to sell an otherwise-great product to.
(And thanks for clarifying the single-threaded aspect.)
And ownership - shared or otherwise - is a great way to eliminate the use-after-free bugs that would otherwise be caused by naively attempting to create a non-owning, borrowing reference to something else. It is in fact the primary use of refcounted pointers in many C++ codebases I've worked with - perhaps with a single std::shared_ptr and several std::weak_ptr s, meaning you're not really sharing ownership per se despite the refcounted nature - or perhaps with multiple std::shared_ptr s out of sheer laziness.
I've had my share of C++ code reviews dinged for perfectly valid code containing non-owning references, simply because such code could be fragile and prone to use-after-free heisenbugs in the face of refactoring, and my reviewers were right to do so. Things are safer in Rust - you can play fast and loose with storing &str s and rely on the borrow checker to catch your mistakes - but playing fast and loose with storing std::string_view s will lead to slipped release dates as you chase 11th hour heisenbugs. Store a std::string or std::shared_ptr<std::string> instead, even if it's "unnecessary".
> there isn't (or at least shouldn't be?) a huge market of people out there using an axe in the kitchen who could've just used a knife instead
Some of us are out camping in the wilderness that is C++ gamedev, and want to make kindling. Do I need to pack a whole axe, or would a knife I need for other reasons anyways make do and lighten my backpack?
And even if that's a small market, it's still entirely a reasonable market, and for advertizing to that audience - however niche - comparing said tools would be an entirely justifyable approach to selling the product.
I also think you underestimate the size of said market.
Uh, I've been wondering at the use case for rc::Weak, that seems like an interesting halfway point if you think borrowing should work but you can't get the compiler to accept it.
One actionable alternative is explicit owner/borrowing smart pointers, which assert if an owner is destroyed while borrowers are pending - which while not eliminating bugs, at least makes them much shallower to debug. I don't have hands on experience with that style - I inherit codebases and their existing coping patterns too often - but I hear good things from those who do. Another actionable alternative is resorting to a proper garbage collector. Or using a proper COM pointer type. Or RIIR. I resort to Rc/Arc in Rust much less than I resort to shared_ptr in C++ (although it still has its niches.)
Then again, I used Arc twice just yesterday:
https://github.com/MaulingMonkey/rust_http_chat_server/blob/...
`Common` (shared between request-handling worker threads) could be made static instead of Arc (although this would eliminate having separate isolated state for, say, different hosts?) Or I could use a scoped thread API that ensures the worker threads are cleaned up before the main thread does, and leave it on the stack instead. The standard library's scoped threads are nightly-only, but there are third party crates - or I could roll my own with `unsafe { ... }`.
`Arc<String>` messages (shared between chat history broadcasting threads until every recipient has sent out a reply) could be simply deep copied. Given the small typical message sizes, that might even be cheaper (although with the heap allocs I wouldn't bet on it.) Or a proper thread safe mpmc FIFO container could be used, although none is in the standard library.
A 200 LOC monolithic main.rs isn't exactly well designed, but it didn't need to be. That's a cop-out, but I suspect your pull requests won't be forthcoming either ;).
C++ is all about stack-bound resource management.
C++ might be getting hazardous pointers thought, although most likely they will only make it to C++26.
I’ve implemented this in my current C++ game engine, and works wondrously well. You can even pack that generational index in the unused 16-bits of a 64-bit pointer, if you have further constraints (like a C library only allowing void* as userdata)!
This is a common misunderstanding.
The key insight of this work is actually "morphing dangling pointers into memory leaks is WAY EASIER than automatic memory management (a.l.a. GC or refcount)". As long as we still do memory management by hand, we are able to avoid most of the complexity of GC (or defects of RC) and convert one bug class to another without much overhead.
Yes, this work can't eliminate bugs in your code (unlike, say, Rust, or other GC languages). But some bugs matter more than others, and, it works on existing code, it makes significant contribution to security without rewriting the world with Rust or JavaScript.
Forever is a long time.
> any help to improve their security is welcomed.
I'm less certain of this than you are, after decades of watching "We may not have really solved the problem, but this helps 99%" almost immediately followed by "Oh, our adversaries apparently aren't idiots and so our 99% mitigation did not actually reduce our burden significantly".
For domains where you don't have security considerations, this is fine. If you make single player video games a mitigation which means you have 4 crash bugs to fix in your game instead of 400 is excellent news. The bugs aren't trying to catch you, so mitigations are effective. But in security your attackers will put no effort into cutting through your expensive thick steel reinforced concrete wall, and focus their effort instead on picking the $4 padlock on your gate in that wall or vaulting over the wall, or just say they're a package delivery and drive in past your guards or a dozen other things you never thought of.
Your game development example, other than Embark Studios, very few care about.
Not to mention that Unreal, Unity, Godot are the bare minimum to beat.
AAA game companies that are security conscious are already using .NET, Java, Go on their game servers.
That's not how forever works.
As to the specifics, there's some sign that things like the GPU APIs are realising they did care about memory safety after all - when the problem caused by a memory safety bug is that some particle effects look a bit wonky every tenth frame in a game and you find zeroing the Z buffer fixes it even though it logically shouldn't that's never getting off the backlog, but when it causes your $10M compute cluster to spit out bogus results unless you reboot it between runs that's $$$ wasted.
I fully expect that for a while they will be enthusiastic about all the other things that attracted people who eventually end up with Rust - they'll purchase static checkers, they'll write code in a different style, they'll invest in more sophisticated debugging tools, but there's a reason Rust happened.
For the compilers, well, I'm old, twenty years ago I was a full-time C programmer, today nobody would do what we did in C back then, maybe they'd try in C++? But the thing which initially attracted people to C++ was that whole "Won't do better without hand-rolled machine code" and that has rotted pretty badly even in twenty years because of... drum roll, ABI compatibility.
The compiler vendors care about performance, long after apparently many C++ programmers are content to produce code with similar performance to Python for ten times the cost and a fraction of the safety, the compiler vendors are still under pressure to ship stuff that has decent performance but also correctness. Rust looks like a better fit for that every year.
With a much smaller size, say, 128 bytes, almost every new allocation could be put into a new page at a new address, with one or more guard pages behind it.
Yes, it would increase the size of the page tables, and the lookup time, but maybe it's worth it.
https://lamport.azurewebsites.net/tla/high-level-view.html
See:
https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines...
and specifically, guidelines R.10, R.1 and R.3.
See also this answer:
https://stackoverflow.com/a/48046118/1593077
to the question of why C++ doesn't have a garbage collector.
To be more concrete, the example would be written today as:
the last line won't compile - foo is gone already; and if you moved it inside the block scope, then foo would be valid.Now, the guidelines I mentioned can also be partially statically-checked, and some IDEs have begun to do so, so that the original snippet would give you at least 3 or 4 warnings. Interestingly, this will be true in a larger codebase as well, despite the claim in the article that
> UAFs are often hard to spot in larger codebases where ownership of objects is transferred between various components.
and that's because if you adhere to the coding guidelines _locally_, but throughout your application, including also I.11 about ownership transfer, then you can never free a pointer you don't own (unless you explicitly decide to do so), and you can even avoid dereferencing null pointers. Yes, I meant what I said: You can simply avoid ever dereferencing a null pointer.
All of this is supported by static analysis, so you absolutely don't need to be a virtuous and skilled coder to avoid such conundrums.
---
There's a caveat though: If some of the code is not under your control, but is rather some 3rd-party library or header, then what I said only applies if you properly "sanitize" at the boundaries of your well-checked.
The shared_ptr is an optional garbage collection based on reference counting. Slow, leaky, deterministic (that's an advantage and a disadvantage). C++ needs optional garbage collection if needs shared_ptr.
The way C++/CLI, C++/CX and Unreal C++ work isn't based on those APIs (naturally, given their domain), and are even used on the C++23 motivation paper as why those GC APIs should be removed.
The article specifically mentions smart pointers and their limitations.
The article describes what it claims to be limited mitigation efforts; but what I commented about is how they make the problem go away, if you actually use them. If you have some smart pointers but also allocate and deallocate memory manually in all sorts of places in your app, and transfer ownership via raw pointers, then - yeah, you might have UAF problems. In 2011 you could have blamed the lack of language features and library facilities to avoid this; but those days are over. UAF problems are now pitfalls you have to actively and knowingly walk into, they don't just happen.
There are other issues with safety which still happen, like out-of-bounds access, which don't have this kind of "effective inoculation"; so it's not as though C++ is now a completely safe language. But UAFs are now mostly a non-issue.
C++98-style actually-existing large production C++ codebases, you mean.
I suppose there's use in mechanisms for improved safety for outdated code, sure.
Investing in mitigations is not free. The time and effort it takes to develop them is not available for constructing bug-free code. And once you have them, there will be even less reason to construct bug-free code. After all the magic hardware-assisted mitigations will save us now.
We've been through this before multiple times now. Someone will find a way to exploit around your mitigation, and you will then double down and come up with the next mitigation that will also be circumvented eventually.
Personally I believe that "retrofitting" security can never work well. Security must be constructed, not retrofitted. Why is this example code using raw allocations? Construct secure code by using containers, not manual allocations. This has been the recommendation for years now.
Something like C++ Core Guidelines, which include your remarks, is what should be looked into.
Naturally Chrome follows Google's C++ guidelines, so.
Certainly the efforts are welcome in any context in regards to C++ security improvements in mitigations, and C as well.
Now, the guidelines aren't the best up to date in what concerns writing secure C++, as per security standards.
Any software with complex object ownership written in a language without GC has to balance between user-after-free (freeing an object too early) and memory leaks (keeping an object alive longer than needed). Maybe constructing secure code here would mean simplifying object ownership, but that doesn't seem practical for a beast such a modern browser with millions of lines of code.
MTE with the heap scanning sounds like a real solution for the user-after-free problem, not merely a mitigation. The trade-off is that the memory is not immediately freed, but the measured increase of only 1% is promising.
- you have some AST object and want to lower it in the compiler into an intermediate representation
- you want to add some interesting parts of the AST to your state while lowering and refer to them later in the lowering process
- This seems fine: the AST is first created, then you lower to the intermediate representation, then you can destroy the AST, so while you’re doing the lowering, the AST objects should all be alive and therefore ok to store references to in the lowerer-state
- However there is some mostly unnoticed code that is roughly desugaring some syntax by constructing temporary AST nodes
- So your addition may have your state including references to these temporary nodes
- And those temporary nodes are freed shortly after being created rather than after the lowering is all done
- Giving a quite subtle use-after-free.
Perhaps the longer-term solution is to use languages that are memory safe by design, such as Rust, to avoid that class of bugs. But there will still be a huge deal of legacy C++ code to contend with in the meantime, so we do still need exploit mitigations.
Clearly you can't expect programmers to just write secure code. If that worked, we would have seen evidence for it working by now.
By construct I mean: follow a clear method or path, that is a) feasible to follow and b) will lead to bug-free code. Maybe not bug free in all respects but I posit it should be possible to construct code without memory safety issues (just look at Perl or Rust).
My point is: It should not be up to the programmer to "simply not make mistakes". The method should be clear and have little ambiguity, and it should be obvious to see if code actually follows it or not.
I can't be more concrete or specific than that because I think we still need to find that method. We should be working on it, though.
I feel like this is a bit of a false dichotomy. Writing bug-free code is one approach to getting bug-free software. Attempting to detect UAF at run time is another approach. And there are many other options beyond those two (static analysis, sanitizers, testing, ...).
In theory, we need only the ability to write bug-free code. If we could do this reliably, repeatedly, in a reasonable amount of time and at a large scale (Chromium, in this case), then there would be no value in investing in any of these other approaches.
In practice, fixing every last bug in a large codebase can take a truly immense amount of time. And that is for a codebase that is not changing at all (including bug fixes!), which certainly does not describe Chromium or most large projects for that matter.
Given all that, it's entirely reasonable to think about how one can find the most bugs with the least amount of effort. And that's basically what this is--another form of automated bug detection. I really don't think it's intended as a substitute for correct code.
I'm not advocating for "fixing all the bugs". On the contrary. I argue that retrofitting security does not work.
I argued that we need to find a way to construct secure software, not to make existing insecure software more secure retroactively. We have tried writing shitty software first and then try to make it more secure for decades now. The track record is not good. We should try something else. Like constructing secure software from the start.