> You should note that Rust does not allow unintialized value by design and thus it does prevent heartbleed from happening. But indeed no programming language will ever prevent logic bugs from happening.
Under OpenBSD, that values would not have been uninitialized, were it not for OpenSSL's silly malloc wrapper -- a contraption of the sort that, if they really wanted, they could probably implement on top of Rust as well. What is arguably a logic mistake compromised the protection of a runtime that, just like Rust, claimed that it would not allow uninitialized values, "by design".
Of course, idiomatic Rust code would not fall into that trap -- but then arguably neither would idiomatic C code. It's true that Rust also enforces some of the traits of its idioms (unlike C), but as soon as -- like the OpenSSL developers did in C, or like Unangst did in that trivial example -- you start making up your own, there's only that much the compiler can do.
At the end of the day, the only thing that is 100% efficient is writing correct code. Better languages help, but it's naive to hope they'll put an end to bugs like these when they haven't put an end to many other trivial bugs that we keep on making since the days of EDSAC and Z3.
> Under OpenBSD, that values would not have been uninitialized, were it not for OpenSSL's silly malloc wrapper -- a contraption of the sort that, if they really wanted, they could probably implement on top of Rust as well. What is arguably a logic mistake compromised the protection of a runtime that, just like Rust, claimed that it would not allow uninitialized values, "by design".
I really disagree. Rust does not allow uninitialized values by design - end of story. If a piece of Rust code let's uninitialized values bleed through, then it is broken. The semantics of Rust demands this.
(OpenSSL on the other hand only broke/Overrode OpenBSD's malloc - they didn't break C.)
It is news to no one that you can break - break - Rust's semantics if you use anything that demands `unsafe`. That's why anyone who uses `unsafe` and intends to wrap that `unsafe` in a safe interface has to be very careful.
Complaining about Rust being unsafe - in the specific sense that the Rust devs use - by using the `unsafe` construct, is like complaining that Haskell is impure because you can use `unsafePerformIO` to `launchMissiles` from a non-IO context.
> Of course, idiomatic Rust code would not fall into that trap -- but then arguably neither would idiomatic C code.
It's not even a question of being idiomatic. If someone codes in safe (non-`unsafe`) Rust, then they should not fall into the trap that you describe. If they do, then someone who implemented something in an `unsafe` block messed up and broke Rust's semantics.
What if that same thing happened in C? Well, then it's just another bug.
---
I'd bet you'd be willing to take it to its next step, even if we assume that a language is 100% safe from X no matter what the programmer does - "what if the compiler implementation is broken?". And down the rabbit hole we go.
> I really disagree. Rust does not allow uninitialized values by design - end of story. If a piece of Rust code let's uninitialized values bleed through, then it is broken. The semantics of Rust demands this. (OpenSSL on the other hand only broke/Overrode OpenBSD's malloc - they didn't break C.)
I'm not familiar enough with Rust (mostly on account of being more partial to Go...), so I will gladly stand corrected if I'm missing anything here.
If the OpenSSL did the same thing they did in C -- implement their own, custom allocator over a pre-allocated memory region, would anything in Rust prevent them from the same sequence of events? That is:
1. Program receives a packet and wants 100 bytes of memory for it.
2. It asks custom_allocator to give it a 100 byte chunk. custom_allocator gives it a fresh 100 byte chunk, which is correctly initialized because this is Rust.
3. Program is done with that chunk...
4. ...but custom_allocator is not. It marks the 100 byte chunk as free for it to use them again, but continues to retain ownership and does not clear its contents.
5. Program receives a packet that claims it has 100 bytes of payload, so it asks custom_allocator to give it a chunk of 100 bytes. custom_allocator gives it the same chunk as before, without asking the Rust runtime for another (initialized!) chunk. Program is free to roam around those 100 bytes, too.
I.e. the semantics of Rust do not allow for data to be uninitialized, but custom_allocator sidesteps that.
It doesn't have custom allocator support as in "you can't have one function allocate memory and pass it for another function to use it", or as in "you can't replace the runtime's own malloc"? OpenSSL were doing the former, not the latter.
(Edit: I'm really really curious, not necessarily trying to prove a point. I deal with low-level code in safety-critical (think medical) stuff every day, and only lack of time is what makes me procrastinate that week when I'm finally going to learn Rust)
Having said that, I hope jemalloc gets linked externally soon so my code doesn't have to pay the memory penalty in each of my memory-constrained threads.
You can't say "this vector uses this allocator and this vector uses another one." If you throw away the standard library, you can implement malloc yourself, but then, you're building all of your own stuff on top of it, so you'd be in control of whatever in that case.
(We eventually plan on supporting this case, just haven't gotten there yet.)
Yes, my point was that OpenSSL did not throw away the standard library! openssl_{malloc|free} were thin wrappers over the native malloc() and free(), except that they tried to be clever and not natively free() memory areas so that they can be reused by openssl_malloc() without calling malloc() again. I.e. sometimes, openssl_free(buf) would not call free(buf), but leave buf untouched and put it in a queue -- and the openssl_malloc() would take it out of there and give it to callers.
So basically openssl_malloc() wasn't always malloc()-ing -- it would sometimes return a previously malloc()-ed aread that was never (actually) freed.
This rendered OpenBSD's hardened malloc() useless: you can configure OBSD to always initialize malloc-ed areas. If OpenSSL had used malloc() instead of openssl_malloc, even with the wrong (large and unchecked) size, the buffer would not have contained previous values, as it would have already been initialized to 0x0D. Instead, they'd return previous malloc()-ed -- and never actually free()-d -- buffers, that contained the old data. Since malloc() was not called for them, there was never a chance to re-initialize their contents.
This can trivially (if just as dumbly!) be implemented in Go. It's equally pointless (the reasons they did that were 100% historical), but possible. And -- assuming they'd have done away with the whole openssl_dothis() and openssl_dothat() crap -- heartbleed would have been trivially prevented in C by just sanely implementing malloc.
> (We eventually plan on supporting this case, just haven't gotten there yet.)
I'm really (and not maliciously!) curious about how this will be done :). You guys are doing some amazing work with Rust! Good luck!
> You should note that Rust does not allow unintialized value by design and thus it does prevent heartbleed from happening. But indeed no programming language will ever prevent logic bugs from happening.
Under OpenBSD, that values would not have been uninitialized, were it not for OpenSSL's silly malloc wrapper -- a contraption of the sort that, if they really wanted, they could probably implement on top of Rust as well. What is arguably a logic mistake compromised the protection of a runtime that, just like Rust, claimed that it would not allow uninitialized values, "by design".
Of course, idiomatic Rust code would not fall into that trap -- but then arguably neither would idiomatic C code. It's true that Rust also enforces some of the traits of its idioms (unlike C), but as soon as -- like the OpenSSL developers did in C, or like Unangst did in that trivial example -- you start making up your own, there's only that much the compiler can do.
At the end of the day, the only thing that is 100% efficient is writing correct code. Better languages help, but it's naive to hope they'll put an end to bugs like these when they haven't put an end to many other trivial bugs that we keep on making since the days of EDSAC and Z3.