Live data from Hacker News

Pitfalls of Safe Rust

corrode.dev

81–90 of 145 posts

Re: Pitfalls of Safe Rust

#81
post #77

> Overflow errors can happen pretty easily No they can’t. Overflows aren’t a real problem. Do not add checked_mul to all your maths. Thankfully Rust changed overflow behavior from “undefined” to “well defined twos-complement”.

The vast majority of code that does arithmetic will not produce a correct result with two's complement. It is simply assuming that the values involved are small enough that it won't matter. Sometimes it is a correct assumption, but whenever it involves anything derived from inputs, it can go very wrong.

For any arithmetic expression that involves only + - * operators and equally-sized machine words, two's complement will actually yield a "correct" result. It's just that the given result might be indicating a different range than you expect.

Re: Pitfalls of Safe Rust

#82
post #58
post #40

Earlier quoted context omitted.

I don't see why people would drop the "memory" part of "memory safe" and just promote the false advertising of "safe rust"

It sounds like you should read the docs. It's just a subject-specific abbreviation, not an advertising trick.

but it is false advertising when it's used all over the internet with: rust is safe! telling the whole world to rtfm for your co-opting of the generic word "safe" is like advertisers telling you to read the fine print: a sleazy tactic.

Re: Pitfalls of Safe Rust

#83
post #39

Earlier quoted context omitted.

Almost exclusively isn't the same as exclusively. Notably the log4shell[1] vulnerability wasn't due to buffer overruns, and happened in a memory safe language. [1]: https://en.m.wikipedia.org/wiki/Log4Shell

In fact "exclusively" doesn't belong in the statement at all. A very small number of successful RCE attacks use exploits at all, and of those, most target (often simple command) injection vulnerabilities like Log4Shell. If you think back to the big breaches over the last five years, though -- SolarWinds, Colonial Pipeline, Uber, Okta (and through them Cloudflare), Change Healthcare, etc. -- all of these were basic ac…

Can you back up your 'very small number " with some data? I don't think it lines up with my own experience here. It's really not an either or matter. Good security requires a multifaceted approach. Memory safety is definitely a worthwhile investment.

Re: Pitfalls of Safe Rust

#84
post #3

Title is slightly misleading but the content is good. It's the "Safe Rust" in the title that's weird to me. These apply to Rust altogether, you don't avoid them by writing unsafe Rust code. They also aren't unique to Rust. A less baity title might be "Rust pitfalls: Runtime correctness beyond memory safety."

It is consistent with the way the Rust community uses "safe": as "passes static checks and thus protects from many runtime errors." This regularly drives C++ programmers mad: the statement "C++ is all unsafe" is taken as some kind of hyperbole, attack or dogma, while the intent may well be to factually point out the lack of statically checked guarantees. It is subtle but not inconsistent that strong static checks ("s…

> This regularly drives C++ programmers mad

I thought the C++ language did that.

Re: Pitfalls of Safe Rust

#85

Earlier quoted context omitted.

Unfortunately, operator[] on std::vector is inherently unsafe. You can potentially try to ban it (using at() instead), but that has its own problems. There’s a great talk by Louis Brandy called “Curiously Recurring C++ Bugs at Facebook” [0] that covers this really well, along with std::map’s operator[] and some more tricky bugs. An interesting question to ask if you try to watch that talk is: How does Rust design aro…

Thank you for sharing. Seems I still have more to learn! It seems the bug you are flagging here is a null reference bug - I know Rust has Optional as a workaround for “null” Are there any pitfalls in Rust when Optional does not return anything? Or does Optional close this bug altogether? I saw Optional pop up in Java to quiet down complaints on null pointer bugs but remained skeptical whether or not it was better to…

Rust’s Optional does close this altogether, yes. All (non-unsafe) users of Optional are required to have some defined behavior in both cases. This is enforced by the language in the match statement, and most of the “member functions” on Optional use match under the hood.

This is an issue with the C++ standardization process as much as with the language itself. AIUI when std::optional (and std::variant, which has similar issues) were defined, there was a push to get new syntax into the language itself that would’ve been similar to Rust’s match statement.

However, that never made it through the standardization process, so we ended up with “library variants” that are not safe in all circumstances.

Here’s one of the papers from that time, though there are many others arguing different sides: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2015/p00...

Re: Pitfalls of Safe Rust

#86
post #17

Earlier quoted context omitted.

Compared to C/C++ "as" feels so much safe r. Now that Rust and we the programmers have evolved with it, I too feel that "as" for narrowing conversion is a small foot gun.

I'm struggling to see how you would implement narrowing conversion in a way that is harder for programmers to misuse when they aren't being mindful, while also being pleasant to use when you really do want to just drop higher bits. Like, you could conceivably have something like a "try_narrow" trait which wraps the truncated value inside an Err when it doesn't fit, and it would probably be harder to accidentally misu…

I don't really want narrowing conversion to be harder, I just want checked conversion to be at least nearly as convenient. `x as usize` vs `x.try_into().unwrap()` becomes `x tiu usize` or something even. I'm not picky. It's kindof funny that this is the exact mistake C++ made, where the safe version of every container operation is the verbose one: `vector[]` vs `vector.at()` or `*optional` vs `optional.value()`, which results in tons and tons of memory problems for code that has absolutely no performance need for unchecked operations.

Re: Pitfalls of Safe Rust

#87
post #49
post #46

Earlier quoted context omitted.

The commonly given response to this question is two-fold, and both parts have a similar root cause: smart pointers and "safety" being bolted-on features developed decades after the fact. The first part is the standard library itself. You can put your data in a vec for instance, but if you want to iterate, the standard library gives you back a regular pointer that can be dereferenced unchecked, and is intended to be i…

Yep. Safe rust also protects you from UB resulting from incorrect multi-threaded code. In C++ (and C#, Java, Go and many other “memory safe languages”), it’s very easy to mess up multithreaded code. Bugs from multithreading are often insanely difficult to reproduce and debug. Rust’s safety guardrails make many of these bugs impossible. This is also great for performance. C++ libraries have to decide whether it’s bett…

I've written some multithreaded rust and I've gotta say, this does not reflect my experience. It's just as easy to make a mess, as in any other language.

Re: Pitfalls of Safe Rust

#88

Earlier quoted context omitted.

There is, since the zero is used as a niche value optimisation for enums, so that Option > occupies the same amount of memory as u32. But this can be used with other enums too, and in those cases, having a zero NonZero would essentially transmute the enum into an unexpected variant, which may cause an invariant to break, thus potentially causing memory unsafety in whatever required that invariant.

> which may cause an invariant to break, thus potentially causing memory unsafety in whatever required that invariant By that standard anything and everything might be tainted as "unsafe", which is precisely GP's point. Whether the unsafety should be blamed on the outside code that's allowed to create a 0-valued NonZero or on the code that requires this purported invariant in the first place is ultimately a matter of…

EDIT: A summary of this is that it is impossible to write a sound std::Vec implementation if NonZero::new_unchecked is a safe function. This is specifically because creating a value of NonZero which is 0 is undefined behavior which is exploited by niche optimization. If you created your own `struct MyNonZero(u8)`, then you wouldn't need to mark MyNonZero::new_unchecked as unsafe because creating MyNonZero(0) is a "valid" value which doesn't trigger undefined behavior.

The issue is that this could potentially allow creating a struct whose invariants are broken in safe rust. This breaks encapsulation, which means modules which use unsafe code (like `std::vec`) have no way to stop safe code from calling them with the invariants they rely on for safety broken. Let me give an example starting with an enum definition:

  // Assume std::vec has this definition
  struct Vec {
    capacity: usize,
    length:   usize,
    arena:    * T
  }
  
  enum Example {
    First {
      capacity: usize,
      length:   usize,
      arena:    usize,
      discriminator: NonZero
    },
    Second {
      vec: Vec
    }
  }
Now assume the compiler has used niche optimization so that if the byte corresponding to `discriminator` is 0, then the enum is `Example::Second`, while if the byte corresponding to `discriminator` is not 0, then the enum is `Example::First` with discriminator being equal to its given non-zero value. Furthermore, assume that `Example::First`'s `capacity`, `length`, and `arena` fields are in the in the same position as the fields of the same name for `Example::Second.vec`. If we allow `fn NonZero::new_unchecked(u8) -> NonZero` to be a safe function, we can create an invalid Vec:

  fn main() {
    let evil = NonZero::new_unchecked(0);
  
    // We write as an Example::First,
    // but this is read as an Example::Second
    // because discriminator == 0 and niche optimization
    let first = Example::First {
      capacity: 9001, length: 9001,
      arena: 0x20202020,
      discriminator: evil
    }

    if let Example::Second{ vec: bad_vec } = first {
      // If the layout of Example is as I described,
      // and no optimizations occur, we should end up in here.

      // This writes 255 to address 0x20202020
      bad_vec[0] = 255;
    }
  }
So if we allowed new_unchecked to be safe, then it would be impossible to write a sound definition of Vec.

Re: Pitfalls of Safe Rust

#89

> Overflow errors can happen pretty easily No they can’t. Overflows aren’t a real problem. Do not add checked_mul to all your maths. Thankfully Rust changed overflow behavior from “undefined” to “well defined twos-complement”.

I'm a big fan of liberal use of saturating_mul/add/sub whenever there is a conceivable risk of coming withing a couple orders of magnitude of overflow. Or checked_*() or whatever the best behavior in the given case is. For my code it happens to mostly be saturating. Overflow bugs are a real pain, and so easy to prevent in Rust with just a function call. It's pretty high on my list of favorite improvements over C/C++

If you saturate you almost never ever want to use the result. You need to check and if it saturates do something else.

Re: Pitfalls of Safe Rust

#90

Earlier quoted context omitted.

What makes you think this is the case? Having done a bunch of formal verification I can say that overflows are probably the most common type of bug by far.

Yeah, they're so common they've become a part of our culture when it comes to interacting with computers. Arithmetic overflows have become the punchline of video game exploits. Unsigned underflow is also one of the most dangerous types. You go from one of the smallest values to one of the biggest values.

Unsigned integers were largely a mistake. Use i64 and called it a day. (Rusts refusal to allow indexing with i64 or isize is a huge mistake.)

Don’t do arithmetic with u8 or probably even u16.

Post reply on HN