Is unsafe Rust worse than C?

Which is why the compiler can't make that guarantee for you. So the coder needs to understand what's going on and decide for themselves. That's not a problem, that's the whole point.

Note that in unsafe Rust, borrow checking still applies. It may be that for a C code that uses ten pointers, the equivalent unsafe Rust code uses seven references and three pointers. In that case, if you get a null pointer exception or a seg fault, in the unsafe Rust code, you only need to check three pointers, in C, you have to check ten.

The basic idea behind Rust's language design is making more type-safe thin abstraction over LLVM optimization rules. In C/C++ these rules are merely considered as best practices that help LLVM find better ways to optimize your code. In Rust they are (to some extent) strong language-level requirements, and their violation result in compile-time errors. The unsafe Rust blocks are fragment of the code where rustc does not perform compile-time checks (on the type level), and it is assumed that you follow the rules manually. Otherwise, the compilation process results in what in Rust world called "undefined behavior". Formally, LLVM's model is not the same thing as Rust's idea of safety, but as I mantioned earlier, they are close to each other.

Writing unsafe code is in fact not too difficult. Using the memory after release is obviously one source of errors, but I assume as a C programmer, you already known that you should avoid such situations.

Another thing, which perhaps may sound lesser familiar, is that you should pay extra attention to how you use memory indirections (references). You can reference memory for read purpose in whatever ways you like, but you should never intermix reads and writes. Write-access should always be exclusive in the control-flow. You can think of memory access as some sort of RAII read-write locks, except that it is you who responsible to avoid concurrent write access (in unsafe code).

The last thing to know is that stack-based memory is more vulnerable to errors than the heap-allocated memory. The main reason for that is that LLVM relies on read-write access rules primarily to improve stackframe utilization in the end codegen. In principal, it is not capable to reason of what is going on in allocator's realm.

There is a number of small nuances that you would learn in time, but I think this information should be enough to start.

I recommend using Rust Playground with Miri. Miri is a Rust tool that helps detecting most of the safety issues in runtime.

Note that it's precisely where DR260 is relevant: it established, more than twenty years ago that there needs to be some kind of rules that deal with these things… but no one was tasked with codification of these rules. Then for a decade compiler developers were happy to just say that these rules are not yet finalized, but when someone else would write them and add to the standard — they would deal with these. And that's how thing went: compiler developers were all-too-happy to have “there should be rules, but they are not codified yet” answer, because it gave them freedom to invent their own rules, compiler users were ignorant, but complained that they valid (note: no quotes, we talk about programs that are strictly standard-conformant in the absence of DR260 decision) programs are miscomputed, but that happened relatively rarely (when compilers were breaking “big” programs like Linux kernel or SPEC CPU benchmarks they have been fixed, which meant many real-world programs worked, too).

Only when Rust people have arrived and started asked questions (you can read about these on Ralf' blog the whole thing have become “a highlighted story”: there were many C and C++ proposals (but nothing was ratified, because standard requires finalized rules — and no one may say “hey, here are the correct an final rules”, at least not yet), Rust got The Tower of Weakenings and we have arrived at the current status quo:

And we have this recurrent discussions where people arrive and talk about how C and C++ are better specified than Rust… nope, they are not better specified, they simply make you believe that.

The question from the title slightly misses the point. Naively replacing C code 1:1 with unsafe Rust code is possible (and can even be automated with c2rust), but leaving it at that is a wrong way to use Rust, and it just gives you a "worse C".

Rust's requirements for unsafe code are similar to C's undefined behavior. Some requirements are more relaxed (there's no TBAA), some are stricter (mutable aliasing and immutability are stronger than in C).

But the point of Rust's unsafe is to build safe abstractions around it.

The unsafe part may be as hard or even trickier than in C, but it's clearly marked, contained, and once it's done and tested, the compiler ensures the safe API is used safely.

Rust's Vec uses unsafe, but the safe public API is as foolproof as lists in Python. You can't do that in C. In C, you can implement a good Vec and give it an API that is possible to use safely, but in C you can't give it an API that is impossible to misuse to cause UB, with zero runtime overhead.

So Rust's unsafe is just as bad or worse than C, but the huge benefit is you can write 99% less of it, which makes writing, testing and fuzzing of the unsafe parts much more tractable.

If memory safety bugs were Waldo (Wally): finding them in C programs is a "Where's Waldo?" game, and Rust's unsafe simplifies it to "Is this Waldo?"

I just read this article: Everything in C is undefined behavior (from stack overflow's newsletter) and I am now convinced that C is worse than unsafe rust

idk, I feel like rust still has a value-add even if you do everything in the "crust" style. The trickier-than-C aliasing rules only apply to references, not raw pointers, so while you're discarding most of Rust's safety checks you also discard much of the UB those checks were protecting you from. Meanwhile you still get nice things like generic data structures, bounds-checked slices, and type-safe discriminated unions.

I think in the future we may see this style of Rust programming more often as people who don't want to fundamentally change how they program try to satisfy regulatory bodies that require them to use memory-safe languages.

If you do everything in crust style your code should definitely not be considered to comply with regulations requiring a memory safe language. The "safe rust" subset is safe, but "unsafe Rust" is not safe.

I think the line is a lot fuzzier than that. It may seem obvious that something written in crust is non-compliant, but what would be compliant, then? 10% unsafe? 2% unsafe? unsafe only in dependencies? If I publish a crates.io package written in crust then import it from my project do I magically become compliant? What about that one package that exploits type-system unsoundness to get a "safe" mem::transmute? In practice I could totally see somebody interpreting a "use memory-safe languages" rule to mean they just need to use rust, not that they need to use it in any particular way.

Wow, I didn't mean to have this thread turn in a Rust vs C discussion. I was mainly meaning to compare how possible it is to write unsafe code in either of them. It seems like it's near impossible in both, which really just further makes me believe there's no point for me to migrate from C to Rust if I still don't know how to write correct code.

The point you seem to be missing is in Rust you do not need to write ANY unsafe code.

Well you did say effectively "Rust is pointless" :face_savoring_food:

The question, really, is about if you, yourself, actually need to use unsafe Rust. It's far less likely than you might think, coming from a C-only background.

As had been said before, that's the actual point of Rust: not writing unsafe code. Just like it's a bad idea to write Java in every language, it's a bad idea to write C in every language. The way to write "Rust code" (as opposed to just "in Rust syntax") is to use the abstractions that unsafe creates. I mean, it's also a bunch of other nice stuff, but that's not really relevant for this topic directly.

This is a really frustrating thread to read seeing people talk about how to avoid writing unsafe, or how specifically not to write unsafe, with no talk on how to really write correct unsafe.

Currently I spent a lot of time writing C code that would have to be written in unsafe Rust. Moving to Rust means there are opportunities for me to make mistakes that wouldn't happen when I write the C code, simply because I don't know how to write unsafe Rust.

There's no real point in switching languages if I get more bugs in my code.

Why do you say it would need to be unsafe Rust? There are times this comes up, but it is for very specific cases. And even then, it is normally (but not always) a minor portion of the code that needs to be unsafe Rust.

The materials describing this were linked above. Are there specific things where you'd like more explanation?

That is true.

Any advice on this topic is detail-specific: at least which part of unsafe Rust are you going to use? There are many, from #[unsafe(no_mangle)] to export functions under their given names, to unsafe impl Send for YourType {} which is about thread safety assertion, to unsafe { ptr.as_mut() }.

An interesting series which might fit you is Learn Rust the Dangerous Way - Cliffle which focuses on porting a C script to Rust 1-to-1 and then reworking it into more idiomatic solution.

Seeing as C often deals in pointer types, here are a few anchor ideas which apply to those:

  1. Pointers are opaque entities which can be quickly converted to and from memory addresses, but are not addresses themselves.
  2. If the pointed-to place may be uninitialized, then you must use the primitives that overwrite it without reading/dropping/otherwise handling the previous value.
  3. Otherwise, you'll want to convert the pointer to shared or mutable(exclusive) reference.
    • &mut T must not overlap any references which it wasn't derived from (those which are up the stack and are effectively inactive until execution flow returns to them).
    • When creating a &T, I imagine the compiler making its own copy of that &T immediately which it can use to do spurious reads and such, and keeping it for as long as you keep your reference.

But that's the thing! It's possible! How? You write your unsafe code in isolation, while implementing algorithms and asking about these on forums (this one and maybe others), using fuzzers, Miri, etc.

That's feasible precisely because 99% of your code shouldn't be unsafe.

With C you would need to do the same — but because there 100% of code is “unsafe” this approach doesn't work.

That is why discussion expanded: the short, naïve answer to “how can I write unsafe Rust” and “how can I write C code” is the same, but with Rust there are also a continutation “… but usually you don't need to write unsafe code at all, use unsafe code written by others”.

It's the same with C#, after all — and people just accept the fact that writing unsafe C# is hard… why should Rust be treated any differently?

Here you are correct: if your plan is to switch to Rust just to write C in Rust then it's not a good idea.

Yet somehow people write a lot of code writing Rust in Rust, from OS kernels and firmware all the way to web seirvice and GUI.

Why do you think you would be writing a lot of unsafe code? It's possible that what you are doing is so extremely unique that approach that all other Rust developers wouldn't work for you… but I, somehow, not convinced: million of developer can build a lot things by avoiding judicious use of unsafe, yet you couldn't? Why?

From what I'm getting so far, the resources on writing unsafe code are to use tools like fuzzers, Miri, asking people online, reading case studies in writing specific unsafe code- Using other people's knowledge. But there's no way to learn how to learn the constraints of writing unsafe code. I just have to hope someone else knows it or another tool picks it up, and I can't find this out for myself?

Edit: Sorry for not replying to the 'you shouldn't be writing unsafe code' posts. I'm not asking how to write safe Rust or how to avoid writing unsafe Rust. That's not the question here. I'm perfectly capable of doing those things.

Please check out the info on this that has been linked (e.g., Is unsafe Rust worse than C? - #2 by nerditation) and ask any questions you have.

I'm also migrant from C to Rust. Actually I prefer Assembler, but C is fine too. I agree with you, when you use unsafe, you do not get much benefits from Rust unless you like some functional programming or something like that, but C++ works even better in the case. So I decided - if I use Rust, unsafe isn't allow for me. It helps to get 'rusty', try.

I appreciate everyone's responses so far. I don't really see a path right now to feeling confident about my knowledge or correctness of unsafe code I write, so I'll just stick with C for now. Maybe I'll re-approach the topic based on the learning resources in a year or so. Or maybe I'm just not able to learn, that's always a possibility.

I'm sorry for making a thread that has such a subjective answer. I think for me I have more confidence writing C as there's not really an expectation to write fully defined C, just C that lacks bugs. With unsafe Rust you are actively pushed to not understand how to write it or use it but also expected to write unsafe code that is perfect against a model that is incomplete and undocumented. I think this is too big of an ask for me, and I don't see any on ramps or paths to learning what I don't know to achieve this.

At least that's how I see the current situation.