Hacker News new | ask | show | jobs
by nerdile 433 days ago
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."

1 comments

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 ("safe Rust") may still leave the possibility of runtime errors. So there is a legitimate, useful broader notion of "safety" where Rust's static checking is not enough. That's a bit hard to express in a title - "correctness" is not bad, but maybe a bit too strong.

No, the Rust community almost universally understands "safe" as referring to memory safety, as per Rust's documentation, and especially the unsafe book, aka Rustonomicon [1]. In that regard, Safe Rust is safe, Unsafe Rust is unsafe, and C++ is also unsafe. I don't think anyone is saying "C++ is all unsafe."

You might be talking about "correct", and that's true, Rust generally favors correctness more than most other languages (e.g. Rust being obstinate about turning a byte array into a file path, because not all file paths are made of byte arrays, or e.g. the myriad string types to denote their semantics).

[1] https://doc.rust-lang.org/nomicon/meet-safe-and-unsafe.html

Mostly, there is a sub culture that promotes to taint everything as unsafe that could be used incorrectly, instead of memory safety related operations.
That subculture is called “people who haven’t read the docs”, and I don’t see why anyone would give a whole lot of weight to their opinion on what technical terms mean
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.
Someone tell that to the standard library. No memory safety involved in non-zero numbers https://doc.rust-lang.org/std/num/struct.NonZero.html#tymeth...
There is, since the zero is used as a niche value optimisation for enums, so that Option<NonZero<u32>> 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.

Because of cult like belief structures growing up around rust, it's clear as day for us on the outside, I see it from the evangelists in the company I work for "rust is faster and safer to develop with when compared to c++", I'm no c++ fan but it's obviously nonsense.

I feel people took the comparison of rust to c and extrapolated to c++ which is blatantly disingenuous.

The cult that I see growing online a lot are those who are invested in attacking Rust for some reason, though their arguments often indicate that they haven't even tried it. I believe that we're focusing so much on Rust evangelists that we're neglecting the other end of the zealotry spectrum - the irrational haters.

The Rust developers I meet are more interested in showing off their creations than in evangelizing the language. Even those on dedicated Rust forums are generally very receptive to other languages - you can see that in action on topics like goreleaser or Zig's comptime.

And while you have already dismissed the other commenter's experience of finding Rust nicer than C++ to program in, I would like to add that I share their experience. I have nothing against C++, and I would like to relearn it so that I can contribute to some projects I like. But the reason why I started with Rust in 2013 was because of the memory-saftey issues I was facing with C++. There are features in Rust that I find surprisingly pleasant, even with 6 additional years of experience in Python. Your opinion that Rust is unpleasant to the programmer is not universal and its detractions are not nonsense.

I appreciate the difficulty in learning Rust - especially getting past the stage of fighting the borrow checker. That's the reason why I don't promote Rust for immediate projects. However, I feel that the knowledge required to get past that stage is essential even for correct C and C++. Rust was easy for me to get started in, because of my background in digital electronics, C and C++. But once you get past that peak, Rust is full of very elegant abstractions that are similar to what's seen in Python. I know it works because I have trained js and python developers in Rust. And their feedback corroborates those assumptions about learning Rust.

Care to explain the obvious, then? Rust is quite a lot nicer to write than C++ in my experience (and in fact, it seems like rust is most attractive to people who were already writing C++: people who still prefer C are a lot less likely to like Rust).
I see this subculture far more in online forums than with fellow Rust developers.

Most often, the comments come from people who don’t even write much Rust. They either know just enough to be dangerous or they write other languages and feel like it’s a “gotcha” they can use against Rust.

Formally the team/docs are very clear, but I think many users of Rust miss that nuance and lump memory safety together with all the other features that create the "if it compiles it probably works" experience

So I agree with the above comment that the title could be better, but I also understand why the author gave it this title

I agree with most of your assertions.

> ... with all the other features that create the "if it compiles it probably works" experience

While it's true that Rust's core safety feature is almost exclusively about memory safety, I think it contributes more to the overall safety of the program.

My professional background is more in electronics than in software. So when the Rust borrow checker complains, I tend to map them to nuances of the hardware and seek work-arounds for those problems. Those work-arounds often tend to be better restructuring of the code, with proper data isolation. While that may seem like hard work in the beginning, it's often better towards the end because of clarity and modularity it contributes to the code.

Rust won't eliminate logical bugs or runtime bugs from careless coding. But it does encourage better coding practices. In addition, the strict, but expressive type system eliminates more bugs by encoding some extra constraints that are verified at compile time. (Yes, there are other languages that do this better).

And while it is not guaranteed, I find Rust programs to just work if it compiles, more often than in the other languages I know. And the memory-safety system has a huge role in that experience.

If a C++ developer decides to use purely containers and smart pointers when starting a new project, how are they going to develop unsafe code?

Containers like std::vector and smart pointers like std::unique_ptr seem to offer all of the same statically checked guarantees that Rust does.

I just do not see how Rust is a superior language compared to modern C++

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 invalidated while still held in the event of a mutation. The second part is third party libraries. You may be diligent about managing memory with smart pointers, but odds are any library you might use probably wants a dumb pointer, and whether or not it assumes responsibility for freeing that pointer later is at best documented in natural language.

This results in an ecosystem where safety is opt-in, which means in practice most implementations are largely unsafe. Even if an individual developer wants to proactive about safety, the ecosystem isn't there to support them to the same extent as in rust. By contrast, safety is the defining feature of the rust ecosystem. You can write code and the language and ecosystem support you in doing so rather than being a barrier you have to fight back against.

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 better to be thread safe (at a cost of performance) or to be thread-unsafe but faster. Lots of libraries are thread safe “just in case”. And you pay for this even when your program / variable is single threaded. In rust, because the compiler prevents these bugs, libraries are free to be non-threadsafe for better performance if they want - without worrying about downstream bugs.

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.
The standard library doesn't give you a regular pointer, though (unless you specifically ask for that). It gives you an iterator, which is pointer-like, but exists precisely so that other behaviors can be layered. There's no reason why such an iterator can't do bounds checking etc, and, indeed, in most C++ implementations around, iterators do make such checks in debug builds.

The problem, rather, is that there's no implementation of checked iterators that's fast enough for release build. That's largely a culture issue in C++ land; it could totally be done.

VC++ checked iterators are fast enough for my use cases, not everyone is trying to win a F1 race when having to deal with C++ written code.
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 around those bugs, and what trade offs does it make?

[0]: https://m.youtube.com/watch?v=lkgszkPnV8g

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 design around the fact that there could be the absence of “something” coming into existence when it should have been initialized

It's not so much Optional that deals with the bug. It's the fact that you can't just use a value that could possibly be null in a way that would break at runtime if it is null - the type system won't allow you, forcing an explicit check. Different languages do this in different ways - e.g. in C# and TypeScript you still have null, but references are designated as nullable or non-nullable - and an explicit comparison to null changes the type of the corresponding variable to indicate that it's not null.
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...

> whether or not it was better to design around the fact that there could be the absence of “something” coming into existence when it should have been initialized

So this is actually why "no null, but optional types" is such a nice spot in the programming language design space. Because by default, you are making sure it "should have been initialized," that is, in Rust:

  struct Point {
      x: i32,
      y: i32,
  }
You know that x and y can never be null. You can't construct a Point without those numbers existing.

By contrast, here's a point where they could be:

  struct Point {
      x: Option<i32>,
      y: Option<i32>,
  }
You know by looking at the type if it's ever possible for something to be missing or not.

> Are there any pitfalls in Rust when Optional does not return anything?

So, Rust will require you to handle both cases. For example:

    let x: Option<i32> = Some(5); // adding the type for clarity

    dbg!(x + 7); // try to debug print the result
This will give you a compile-time error:

     error[E0369]: cannot add `{integer}` to `Option<i32>`
       --> src/main.rs:4:12
        |
    4   |     dbg!(x + 7); // try to debug print the result
        |          - ^ - {integer}
        |          |
        |          Option<i32>
        |
    note: the foreign item type `Option<i32>` doesn't implement `Add<{integer}>`
It's not so much "pitfalls" exactly, but you can choose to do the same thing you'd get in a language with null: you can choose not to handle that case:

    let x: Option<i32> = Some(5); // adding the type for clarity
    
    let result = match x {
        Some(num) => num + 7,
        None => panic!("we don't have a number"),
    };

    dbg!(result); // try to debug print the result
This will successfully print, but if we change `x` to `None`, we'll get a panic, and our current thread dies.

Because this pattern is useful, there's a method on Option called `unwrap()` that does this:

  let result = x.unwrap();
And so, you can argue that Rust doesn't truly force you to do something different here. It forces you to make an active choice, to handle it or not to handle it, and in what way. Another option, for example, is to return a default value. Here it is written out, and then with the convenience method:

    let result = match x {
        Some(num) => num + 7,
        None => 0,
    };

  let result = x.unwrap_or(0);
And you have other choices, too. These are just two examples.

--------------

But to go back to the type thing for a bit, knowing statically you don't have any nulls allows you to do what some dynamic language fans call "confident coding," that is, you don't always need to be checking if something is null: you already know it isn't! This makes code more clear, and more robust.

If you take this strategy to its logical end, you arrive at "parse, don't validate," which uses Haskell examples but applies here too: https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-va...

To add on another pitfall: iterator invalidation. In C++ you generally aren't allowed to modify a container while you're iterating through it, because it may re-allocate the memory and leave dangling pointers in the iterator, but the compiler doesn't check this. Rust's lifetime analysis closes this particular issue.

(Basically, the 'newer' C++ features do help a little with memory safety, but it's still fairly easy to trip up even if you restrict your own code from 'dangerous' operations. It's not at all obvious that a useful memory-safe subset of C++ exists. Even if you were to re-write the standard library to correct previous mistakes, it seems likely you would still need something like the borrow checker once you step beyond the surface level).

Here's a program that uses only std::unique_ptr:

  #include<iostream>
  #include<memory>
  
  int main() {

      std::unique_ptr<int> null_ptr;
    
      std::cout << *null_ptr << std::endl; // Undefined behavior
  }
Clang 20 compiles this code with `-std=c++23 -Wall -Werror`. If you add -fsanitize=undefined, it will print

  ==1==ERROR: UndefinedBehaviorSanitizer: SEGV on unknown address 0x000000000000 (pc 0x55589736d8ea bp 0x7ffe04a94920 sp 0x7ffe04a948d0 T1)
or similar.
C++ devs need to understand the difference between:

   Vec1[0];
   Vec1.at(0);
Even the at method isn’t statically checked. If you want static checking, you probably need to use std::array.
Many also need to learn that there are configuration settings on their compilers that make those two cases the same, enabling bounds checking on operator[]().
Sure, but at() is guaranteed to throw an exception and operator[] can throw an exception when you go out of bounds. C++26 is tweaking this, but it's still going to differ implementation to implementation.

At least that's my understanding of the situation. Happy to be corrected though.

The problem with the title is that the phrase "pitfalls of safe rust" implies that these pitfalls are unique to, or made worse by, safe rust. But they aren't. They are challenges in any programming language, which are no worse in rust than elsewhere.

It's like if I wrote an article "pitfalls of Kevlar vests" which talked about how they don't protect you from being shot in the head. It's technically correct, but misleading.

> This regularly drives C++ programmers mad

I thought the C++ language did that.

It certainly used to, but tbh C++ since 17 has been pretty decent and continually improving.

That said, I still prefer to use it only where necessary.

Safe Rust code doesn't have accidental remote code execution. C++ often does. C++ people need to stop pretending that "safety" is some nebulous and ill-defined thing. Everyone, even C++ people, shows perfectly damn well what it means. C++ people are just miffed that Rust built it while they slept.
Accidental remote code execution isn't limited to just memory safety bugs. I'm a huge rust fan but it's not good to oversell things. It's okay to be humble.
RCEs are almost exclusively due to buffer overruns, sure there are examples where that’s not the case but it’s not really an exaggeration or hyperbole when you’re comparing it to C/C++
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

The recent postgresql sql injection bug was similar. It happened because nobody was checking if a UTF8 string was valid. Postgres’s protections against sql injection assumed that whatever software passed it a query string had already checked that the string was valid UTF8 - but in some languages, this check was never being performed.

This sort of bug is still possible in rust. (Although this particular bug is probably impossible - since safe rust checks UTF8 string validity at the point of creation).

This is one article about it - there was a better write up somewhere but I can’t find it now: https://www.rapid7.com/blog/post/2025/02/13/cve-2025-1094-po...

Rust’s static memory protection does still protect you against most RCE bugs. Most is not all. But that’s still a massive reduction in security vulnerabilities compared to C or C++.

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 account takeovers.

To the extent that anyone has to choose between investing in "safe" code and investing in IT hygiene, the correct answer today is IT hygiene.

Research I've seen seems to say that 70-80% of vulnerabilities come from memory safety problems[0]. Eliminating those is of course a huge improvement, but is rust doing something to kill the other 20-30%? Or is there something about RCE that makes it the exclusive domain of memory safety problems?

[0] For some reason I'm having trouble finding primary sources, but it's at least referenced in ex. https://security.googleblog.com/2024/09/eliminating-memory-s...

Rust also provides guarantees that goe beyond mere memory safety. You get data-race safety as well, which avoids certain kinds of concurrency issues. You also get type-safety which is a step up when it comes to parsing untrusted input, at least compared to C for example. If untrusted inout can be parsed into your expected type system, it's more likely to not cause harm by confusing the program about what's in the variables. Rust doesn't straight up eliminate all source of error, but it makes major strides forward in areas that go beying mere memory safety.
If english had static checks this kind of runtime pedantry would be unnecessary. Sometimes it's nice to devote part of your brain to productivity rather than checking coherence.