Hacker News new | ask | show | jobs
by saagarjha 1611 days ago
This article has a core point which is good: “in Rust the default is safe, and you have to opt-in to unsafety, but in C++ the default is unsafe, and you have to opt-in to safety”. I think it’s easy to argue for Rust using this construction, because, well, that’s the entire point why Rust was created.

But, it really doesn’t take a very long post to talk about this. The remainder goes off the rails, talking about “C++ apologists” (hint: if you’re being “fair”, pick words that are unlikely to cause people to be preemptively upset. This is not one of those words) and their stupid opinions. And the author just trashes them as being complete idiots, but it’s obvious that the arguments come from inexperience or strawmen, which just makes the overall thing not particularly convincing. Saying that the various UB finding tools were useless because you tried using them and didn’t get good results is stupid. Being smug about “people who use modern C++ clearly can’t do HFT, which is the thing that you said you were using C++ to do” is also insipid, just because you spotted the use of a shared_ptr somewhere and read how it’s not zero-cost. Modern C++ has other things in it, you know, many of which are zero-cost and significantly (but not entirely) safer; picking one thing and misrepresenting it does not make for a good refutation.

Anyways, coming from someone who writes a lot of C++ and would also like a lot of code to be migrated to Rust for good reasons, it’s a good idea to approach the tradeoffs honestly and without disdain for those who aren’t convinced yet. The core argument I mentioned above and the closing part of the article does do this…but there’s a lot in the middle that doesn’t, and it drags down the usefulness of the post.

1 comments

> This article has a core point which is good: “in Rust the default is safe, and you have to opt-in to unsafety, but in C++ the default is unsafe, and you have to opt-in to safety”.

On the subject of safe defaults, just to correct that Rust does not in fact have as much default memory safety with regards to buffer bleeds (e.g. variants of OpenSSL's Heartbleed) as it could [1], because it has unchecked arithmetic (integer wraparound) as the default for performance, with checked arithmetic only as an opt-in for safety.

In other words, if an attacker can get some bounds merely to underflow (as opposed to overflow) then they can still read the sensitive memory of a Rust program, even without a UAF or buffer overflow.

Bleed vulnerabilities like these are also low-hanging fruit and significantly easier to exploit.

In other words, bounds checking only ensures you are within the buffer, but checked arithmetic is still needed to ensure that your index was correctly calculated in the first place.

I believe that Rust would be much safer against memory bleeds, if it had checked arithmetic enabled by default for safety, with an opt-out at the block scope level for performance, like Zig has.

[1] https://news.ycombinator.com/item?id=29991439

Do you have examples of this actually causing security problems in Rust code at anywhere near the levels of memory safety problems in C/C++?

It strikes me as extremely dubious to tout Zig, which doesn't have memory safety at all, as somehow superior to Rust because Zig has this mitigation enabled by default (with a large performance cost) for an error class that Rust forestalls the vast majority of the negative consequences of, by dint of memory safety.

> Do you have examples of this actually causing security problems in Rust

Do we need them though? I don't believe we need to go through a Heartbleed moment for Rust before we realize that checked arithmetic is just a good idea. We should be able to learn from the history of past exploits, even in other languages, not only in Rust.

I've also worked in the security industry. I've written static analysis software to detect zero-days. I've done professional bug bounty engagements. I know how to hack systems and I know from experience that checked arithmetic is important and something we should care more about as programmers.

Many programmers don't know about buffer bleeds and how dangerous are. There's a whole class of memory exploits that are much easier to pull off than a UAF. Security is about defense-in-depth. Why ship with an unsafe default?

> It strikes me as extremely dubious to tout Zig, which doesn't have memory safety at all

Comparing defaults with respect to checked arithmetic is something that programmers should be open to thinking and talking about. There's also no need to dismiss Zig's spatial memory safety as "no memory safety at all". Spatial memory safety is a pretty big win already. It rules out another class of exploits.

With respect to enabling checked arithmetic by default for safety, with an opt-out for performance at block scope level, Zig arguably has a safer default, something that I wish Rust would also adopt.

I don't agree that that's too "extremely dubious" to ask for?

> (with a large performance cost)

Not so. I'm working on a project that processes a million transactions a second.

We don't see any impact from this because the data plane is clearly delineated from the control plane. Checked arithmetic is enabled everywhere in fact because bounds checks are amortized across larger buffers. Compared to the cost of the cache misses to process the data, the costs of the arithmetic of the bounds check and the branch across the larger buffer are an order of magnitude less.

You can also opt-out at block scope level for hot loops. But we haven't needed to.

Again, projects should rather enable checked arithmetic by default for safety and then profile. Turning it off by default at the language layer with an opt-in for safety, in safe builds, just doesn't seem like the right default to me.

We can agree to disagree.

The data clearly show a significant overhead for integer bounds checks. https://danluu.com/integer-overflow/

At scale this adds up to millions of dollars. It would be a deal breaker for switching to Rust for many organizations. That's why bounds checks in release mode are not the default.

> At scale this adds up to millions of dollars.

Buffer bleeds like Heartbleed cost the industry hundreds of millions of dollars.

Also, to be fair, Dan's post is about checked arithmetic in hot loops, i.e. the data plane, which as I've said, "large organizations" would know to amortize by using large buffers, and by clearly delineating between data plane and control plane.

For example, why not simply disable checked arithmetic at block scope level for a hot loop? Disabling at program level by default, and then having to re-enable it everywhere that's not a hot loop, just seems like conflating data plane and control plane, and like a massive overly big hammer.

It's also dangerous, because unsafe defaults might be run by new programmers who don't understand the risks of unchecked arithmetic, and who think that Rust gives them 100% memory safety.

Also, do you think that a 5% penalty on control planes is cost-prohibitive? I don't.

Control planes are usually where "large organizations" have tons of assertions anyway, for example, AWS really like to run their control planes at constant max load, regardless of actual load, to avoid cascading failure. That costs them millions of dollars, but relative to the hundreds of millions of dollars that their data planes cost, it's worth it, because it saves outages and failures that could easily dwarf the 5% performance gains at the expense of safety.

Safety becomes much more critical at large scale in fact, more so than performance. Better to be correct first, and then fast. Than fast, but not correct.

I’m generally in the favor of checked arithmetic by default, yes. But I will give Rust credit in that it makes this easy to enable and also provides access to the various other kinds of arithmetic readily.
I don't think integer overflow is an especially common source of buffer overflow. This isn't based on any hard data, but I'm pretty sure that the 2 main types of buffer overflow come from

1. not doing the bounds check. 2. not storing the the bounds with the array.