Live data from Hacker News

How memory safety CVEs differ between Rust and C/C++

kobzol.github.io

241–250 of 270 posts

Re: How memory safety CVEs differ between Rust and C/C++

#241
post #70

Earlier quoted context omitted.

It's surprising and potentially buggy behavior, but that's very different from undefined behavior. To such a degree that I think you honestly might not understand what it means, and what the risks are around undefined behavior, especially in the presence of an optimizing compiler. As a starter / refresher perhaps, both of these are perfectly permissible and happen in practice with UB, but never with "wrap or panic" /…

-fsantize=integer and whambo bambo C/C++'s "time traveling UB" is now more consistent, better defined, and safer than Rust's release build behavior. I don't know if Rust will ever attempt to fix this mistake, but I seriously hope they do.

In C/C++ the default behavior, always, is very dangerous UB. Some compilers offer an option for guaranteed wrap-around, and some compilers offer an option for controlled crash. None of them is standardized or the default.

In Rust the standard mandates a guaranteed wrap-around or a controlled crash. The default behavior is crashing in debug mode and wrapping-around in release mode, but you can control that with a compiler flag.

No matter how you look at it, the Rust behavior is safer. The default behavior? Way better in Rust than in C++. Want a guaranteed wrap-around? In Rust, check. In C++, depending on the compiler. Want a guaranteed crash? In Rust, check. In C++, depending on the compiler.

Also worth noting that overflowing has much more severe consequences in C++ than in Rust, due to bounds checks.

> I don't know if Rust will ever attempt to fix this mistake

It was discussed many times, and the conclusion is: it is too expensive, and the default will only change if that will change (due to better optimizations and/or better hardware). It is not a mistake, but a conscious decision.

Re: How memory safety CVEs differ between Rust and C/C++

#242

Earlier quoted context omitted.

-fsantize=integer and whambo bambo C/C++'s "time traveling UB" is now more consistent, better defined, and safer than Rust's release build behavior. I don't know if Rust will ever attempt to fix this mistake, but I seriously hope they do.

In C/C++ the default behavior, always, is very dangerous UB. Some compilers offer an option for guaranteed wrap-around, and some compilers offer an option for controlled crash. None of them is standardized or the default. In Rust the standard mandates a guaranteed wrap-around or a controlled crash. The default behavior is crashing in debug mode and wrapping-around in release mode, but you can control that with a comp…

> > I don't know if Rust will ever attempt to fix this mistake

> It was discussed many times, and the conclusion is: it is too expensive, and the default will only change if that will change (due to better optimizations and/or better hardware). It is not a mistake, but a conscious decision.

There's also wrapping_add / saturating_add for anyone concerned about this. Ya want a specific behavior? Just tell the compiler! You can also use the type wrappers so it happens implicitly! https://doc.rust-lang.org/std/num/struct.Saturating.html (presumably this is optimizable on hardware that has operators for this, but idk if that happens yet)

Re: How memory safety CVEs differ between Rust and C/C++

#243
post #111

Earlier quoted context omitted.

> Rust's standard library is incredibly thin (intentionally so). As a result, you need to use the crate ecosystem. This comes with some downsides. This is no different than C++. C++ standard library made so many compromises in the name of ABI compatibility almost none of the library is actually usable for any use case. So people start to quickly add things like boost, abseil, folly, Qt, asio, imgui, doctest etc. Ther…

I've never seen a C program use hundreds of dependencies. This is typical in Rust (and Node). I know a few high assurance teams that dropped Rust for this very reason.

https://archlinux.org/packages/extra/x86_64/blender/

https://archlinux.org/packages/extra/x86_64/apache/

They look no different than your usual Rust crate. And their full flattened dependency trees already exceeds hundreds of packages.

C/C++ libraries have much more complex build systems with many optional features shipped inside the library. Just think of curl and how many protocols it supports.

Rust's build system, however, is extremely simplistic and limited. So to have things like multiple backends for rendering, parsing, serializing etc. you have to split your library into multiple crates due to limitations Cargo impose on you. So the full equivalent of curl will be 20+ individual crates.

I think the hundreds of dependencies is overblown due to this effect. I maintain my argument. C and C++ projects are just as complex and vulnerable. CMake and fully binary distribution via Linux package managers just hide their complexity.

Re: How memory safety CVEs differ between Rust and C/C++

#244
post #169

Earlier quoted context omitted.

> You cannot save the world from scratch in every little function. You still have a job to get done, and you have to move on. Or you can take all of 10 minutes to put sanity-check assertions at the start of all your public-facing API functions, eliminating a source of security bugs, get on with your life, and worry about the performance implications as and when it becomes a problem (hint: it's never going to become a…

You can try and do this if it's a relatively narrow public facing API, but otherwise this is a theoretic ideal. In practice, if you add an assertion for every pointer argument to every little function, you'll go insane, and it is completely pointless, and the code will not be readable anymore. There are so many other interesting and relevant invariants that are usually in an API contract that are much harder or impos…

No, people focus on a handful of things like null, buffer overrun, and use-after-free because they still make up the majority of security vulnerabilities that we see exploited in the wild. You may imagine that subtle logic errors are more common, but the data doesn't bear that out; also FWIW I've never seen one of these detailed invariants be impossible to express in a type system if you spend 5 minutes actually trying.

Re: How memory safety CVEs differ between Rust and C/C++

#245
post #222
post #212

Earlier quoted context omitted.

There are certainly good parts in Rust that force you deal with inherent issues explicitly. I do not think this justifies a language with the complexity of Rust, but I would agree it is preferable to C++ in this regard.

I reject the whole Rust has a high complexity premise, but I would gladly continue our conversation around it here but I would need a little more definition for that to be possible. What do you mean by complexity in this context and in what way does Rust carry a lot of it?

I did not know this was disputed, as even developer surveys in the Rust community highlighted complexity as a concern. But seems it is the overall summary of features that make it complex: lifetime, borrowing, a polymorphic type system, traits, multiple tools to manage lifetimes, macros, proc macros, async, unsafe, panic, unwrap, etc. But I agree it is much cleaner than C++.

Re: How memory safety CVEs differ between Rust and C/C++

#246
post #244

Earlier quoted context omitted.

You can try and do this if it's a relatively narrow public facing API, but otherwise this is a theoretic ideal. In practice, if you add an assertion for every pointer argument to every little function, you'll go insane, and it is completely pointless, and the code will not be readable anymore. There are so many other interesting and relevant invariants that are usually in an API contract that are much harder or impos…

No, people focus on a handful of things like null, buffer overrun, and use-after-free because they still make up the majority of security vulnerabilities that we see exploited in the wild. You may imagine that subtle logic errors are more common, but the data doesn't bear that out; also FWIW I've never seen one of these detailed invariants be impossible to express in a type system if you spend 5 minutes actually tryi…

Typical invariant for me would look like:

Given a, b, c input parameters to my func, it must hold that that a->m->t == b->t. c->mutex must be held, and c->cond is the condition variable that goes with c->mutex and will release any waiters on the buffer contained in a.

Or: Integer x is representable using 12 bits only, Integer y should be a multiple of N and I have a integer s is used as a bit-shift that should be less than 8.

Or: I need to guarantee that no locks have to be taken and no allocations have to be made on this complicated looking codepath. While holding a lock, we must not do any syscalls (syscall a, b, c are ok though), and surely not make any logging calls.

I know only one system that can express this, it's called STRAIGHTFORWARD CODE, and it requires doing engineering and casual logic out-of-band, and yes it does include making mistakes and repairing them incrementally.

I don't know a type system that would let me explain these things to me and tell me where I was wrong. But maybe you can show me, with 5 minutes of actually trying?

Re: How memory safety CVEs differ between Rust and C/C++

#247

Earlier quoted context omitted.

So reject as in assert? But how does that go together with what you said, "because now it's fine if it does"?

Because no one is expecting it to work if a null is passed. Your total range of behaviours left are crashes, doesn't crash and is silently ok, or doesn't crash and causes something worse (data corruption, you get your product in a CVE, that area). My proposition is that "it's silently ok" isn't likely enough, which is in line with your position on "don't extend the contract to accept null". So what's left is crash, o…

If you follow that line of reasoning, you will end up testing almost every pointer before accessing it. The reason is that you are extending your valid state space massively since you aren't able to specify "this subset of 7 trillion distinct states is invalid, if it was the case we would have failed before".

You are requiring yourself to find a valid outcome for an input that doesn't make _any_ sense in the context of what your application is meant to achieve. How is that not a Sysiphean task?

Re: How memory safety CVEs differ between Rust and C/C++

#248
post #245
post #222

Earlier quoted context omitted.

I reject the whole Rust has a high complexity premise, but I would gladly continue our conversation around it here but I would need a little more definition for that to be possible. What do you mean by complexity in this context and in what way does Rust carry a lot of it?

I did not know this was disputed, as even developer surveys in the Rust community highlighted complexity as a concern. But seems it is the overall summary of features that make it complex: lifetime, borrowing, a polymorphic type system, traits, multiple tools to manage lifetimes, macros, proc macros, async, unsafe, panic, unwrap, etc. But I agree it is much cleaner than C++.

Still not picking on C++ here. I would claim that these concepts pretty much all exist in C++ too, but often implicitly so. You would solve them by "braining them", with good rules of thumb, patterns, tests and deep knowledge and skill. In fact, you _have_ to solve them this way.

In Rust the compiler is going to have a problem with your code unless you are explicit about borrowing/ ownership, gcc won't care. Which is the more complex thing there?

Same goes for the polymorphism. Looking at languages without parametric polymorphism, you instead see casts and inheritance trees. Not picking on old Java here, but is this more or less complex?

Let's talk some about unsafe as well. In C or C++, for instance, there is a global unsafe surrounding all the code, so if someone has problem with unsafe, why would they pick on Rust where "unsafety" is optional?

Same goes for unwrap. Unwrap is basically a deref combined with a _safe_ crash if the deref was not successful at runtime. Do that in an unsafe language and you have an access violation best case. Or just silent corruption. Which is more complex?

Re: How memory safety CVEs differ between Rust and C/C++

#249

Earlier quoted context omitted.

Sure, to simplify lets assume a 64-bit CPU (this all works for 32-bit but that's less common these days and the actual numbers are different) C++ std::string can contain up to 15 (other popular implementations) or 22 bytes (libc++ from Clang) of inline text, and the data structure itself is either 24 bytes (Clang again) or 32 bytes of storage. Here's Raymond Chen: https://devblogs.microsoft.com/oldnewthing/20240510-0…

Thanks! How do CompactString/ColdString compare to std::string implementations performance-wise? From the looks of it, they must be somewhat slower than C++ strings

I do not have hard numbers - however keep in mind that practical "performance" also includes memory bandwidth and total RAM, this is especially a consideration for the ColdString type - a billion ColdStrings is 8GB of RAM, but a billion MSVC std::string needs 32GB of RAM. Rust's std::string::String is of course much faster than any of the std::string implementations because it never has the SSO case to consider - but for non-empty strings it's also more memory bandwdith and RAM needed.

Re: How memory safety CVEs differ between Rust and C/C++

#250
post #244

Earlier quoted context omitted.

No, people focus on a handful of things like null, buffer overrun, and use-after-free because they still make up the majority of security vulnerabilities that we see exploited in the wild. You may imagine that subtle logic errors are more common, but the data doesn't bear that out; also FWIW I've never seen one of these detailed invariants be impossible to express in a type system if you spend 5 minutes actually tryi…

Typical invariant for me would look like: Given a, b, c input parameters to my func, it must hold that that a->m->t == b->t. c->mutex must be held, and c->cond is the condition variable that goes with c->mutex and will release any waiters on the buffer contained in a. Or: Integer x is representable using 12 bits only, Integer y should be a multiple of N and I have a integer s is used as a bit-shift that should be les…

> it must hold that that a->m->t == b->t.

So define a wrapper type that represents that invariant (it's not going to take up space at runtime), where the only constructor enforces it?

> Integer x is representable using 12 bits only, Integer y should be a multiple of N and I have a integer s is used as a bit-shift that should be less than 8.

Those are all standard things that already exist?

> Or: I need to guarantee that no locks have to be taken and no allocations have to be made on this complicated looking codepath. While holding a lock, we must not do any syscalls (syscall a, b, c are ok though), and surely not make any logging calls.

Sounds like a pretty standard free monad case? Define a command algebra in which the "ok" syscalls are a subtype, and then require that the thing you want to only use the ok calls to have a type that reflects that?

Post reply on HN