6 ms·
Genuine question for Rust programmers: Memory safety without GC is a big selling point of Rust. However, sometimes your program’s ownership model does not fit
by pharmakom 5y ago
Genuine question for Rust programmers:
Memory safety without GC is a big selling point of Rust. However, sometimes your program’s ownership model does not fit with what the Rust borrow-checker can verify. Not to worry, just use “unsafe”. But if “unsafe” is used, can we still claim that Rust code is safe?
- Gaelan 5y agoGenerally, the idea is that you use a small amount of unsafe code to build a safe abstraction to do what you need, then use that abstraction in many places. So now you only have a few unsafe sections you have to verify for correctness, instead of the entire program like you'd need in C/C++.
- rapsey 5y ago> However, sometimes your program’s ownership model does not fit with what the Rust borrow-checker can verify. This tends to be a beginner issue for c/c++ programmers. 99% of the time you just need to rethink your design into something better.
- nchi3 5y agoNo, but it makes it explicit regarding what's unsafe, so that you'll more easily know where to put resources to make the unsafe code as bomb proof as possible. In C that's 100% of the code, in Rust you can keep it to a low percentage.
- foldr 5y agoIdeally the unsafe parts are restricted to modules which have been proven safe to an appropriate degree of confidence. ('Unsafe' in Rust really means 'not guaranteed by the type system to be safe', so it's not a contradiction in terms to show that 'unsafe' code is safe.) One might respond that you can do the same kind of modularization in C++. However, I think a Rust advocate would argue that it's much more difficult in practice to modularize unsafe code in C++ to the same degree, since unsafe idioms are deeply embedded in the language and library ecosystem.
- dochtman 5y agounsafe code blocks are required to uphold the invariants of the safe Rust code surrounding it. So yes, if you fail to uphold those invariants (and that's proven to be pretty tricky), your program is not safe. My experience is that you hardly ever need unsafe Rust code. If your ownership model doesn't the borrow checker, there are of course also other avenues like ref counts or even an actual garbage collecter (which you can chose to apply only to values whose ownership situation is not aligned with the stack). Only when those options are unable to give you the performance you want you'll need to reach for unsafe code. Other scenarios where you'll need unsafe might include things like SIMD (although safe wrappers exist) or FFI, but hopefully we'll evolve the Rust ecosystem over time to concentrate the unsafe code in a few places by building abstractions, where they are thoroughly reviewed and well tested.
- netheril96 5y ago> My experience is that you hardly ever need unsafe Rust code. I doubt that experience applies equally to kernel development.
- _flux 5y agoThey may apply better than one might think: the example SPI driver posted at https://lwn.net/Articles/863459/ https://lwn.net/Articles/863459/ only has two cases of unsafe. Of course, the kernel:: -modules it uses undoubtedly have more, but I imagine the idea is to provide abstractions that allow actual driver code not to use unsafe. And then there's stuff like file system code, that—I imagine—would require no more uses of unsafe than a database server written in Rust.
- tialaramex 5y agoBecause it's normal (indeed expected) to build safe abstractions on top of necessarily unsafe code, a lot of the kernel by volume shouldn't need to be unsafe at all. For example suppose that we're back in the late 1990s and so for some reason dozens of different yet roughly equivalent 100Mbps fast Ethernet PCI cards are available, each offering the same functionality but with slightly different register layouts, interrupts and quirks. We are of course writing drivers for them in Rust. Obviously at some fundamental level "configure PCI bus" isn't safe - if you get it wrong that'll be bad. But as the author of mediocre Ethernet driver #42 you aren't implementing "configure PCI bus" you're just calling a safe abstraction that worked for the other 41 drivers. Handling userspace calls to twiddle the Ethernet interface is potentially unsafe too, but likewise that will have been factored out into common code and you're just talking to a safe abstraction. Rust's type system and borrow checking makes it easier to write a safe abstraction for relatively low level ideas like "Only one device can own this MMIO register" or even "this can be 0x03 or 0x06 or even 0x18 but it can't be all zeroes, that's not a thing". This sort of stuff is boring and in principle already not particularly unsafe in C today, this sort of abstraction is already done - but of course in C you've no practical way to get the promises Rust offers for safe code.
- twic 5y agoCode using unsafe might be safe, it just can't be proved to be safe by the compiler. Just as code written in C might be safe, it just can't be proved to be safe by the compiler! Rust's advantage over C is that the machine-unprovability is confined to small, defined zones, where hopefully human programmers can, formally or informally, prove the code safe.
- liaukovv 5y agoUnsafe in rust means "you have to enforce the use contract manually as opposed to compiler doing it for you" So yes, if contract is violated there the entire program is contaminated
- josalhor 5y agoYes. The idea is to isolate the use of unsafe and prove that it is safe. Because compiler cannot prove that what you're doing is safe it is your responsibility to do so. Once you've _proven_ that your unsafe code is safe, then it cannot have propagating effects in the rest of the code. A lot of people get this confused. They think unsafe means you can do whatever you want. In reality it means you'll play by the rules even if the compiler cannot verify them all. In truth it is not as hard as it sounds, we do it in C all the time.
- dgb23 5y agoShould have been called „trust_me“ instead of „unsafe“.
- masklinn 5y agoIt’s never really going to be perfect e.g. your idea doesn’t really work with unsafe functions, which can only be called from unsafe contexts but also are an unsafe context. `unsafe` in rust really means that some of the compiler guardrails are disabled: * you can call unsafe (and extern) functions * you can dereference raw pointers (this has significant consequences such as being able to create mutable references out of thin air) * you can access or modify mutable statics * you can access `union` contents
- Someone1234 5y agoIt could, it would be accurate, but they're trying to implicitly teach this idea to "avoid these." trust_me blocks lacks that implication. It is very easy, particularly coming from another language, to butt heads with the language's design and then try to solve it with "trust_me" blocks. Throwing everything in unsafe feels like you're doing it wrong, and correctly so.
- dgb23 5y agoMy above comment was meant to be an illustration rather than a proposal. I think calling it "unsafe" is a good design choice as you explained. The downside is: Every time this discussion comes up, someone has to explain what "unsafe" actually means. Every single tech talk ever that touches on "unsafe", pre-fixes the talk with this explanation too. A more funny idea instead of "trust_me" would be "safe_because[...]" where "..." is a rationale about the code plus your social security number and biometrics data. For any control freaks and regulators out there: If you take the above seriously and try to implement such a thing you will be burnt by the stake by cyber elves.
- null0pointer 5y agoThe way I view it is that the unsafe keyword acts as a flag for auditors that they would need to thoroughly check the following block for memory errors.
- BiteCode_dev 5y agounsafe rust code is not safe, but the rest of the program is, and the unsafe blocks provides very nice benefits: - limit the size and scope of the unsafe code - make the unsafe part stand out, "here be dragons", so you know when to be particularly alert - act as a safe way to encapsulate unsafe code so that it can be used by safe code, and used without the lack of safety propagating outside of the unsafe zone
- the-smug-one 5y agoYes, we can claim so because there are other ways of proving your program to be safe. See: https://plv.mpi-sws.org/rustbelt/ https://plv.mpi-sws.org/rustbelt/ By the way, several parts of the std (Mutex and Arc for example) are written in unsafe Rust. They have to be, by necessity, because they allow for aliasing and mutation.
- oleganza 5y agounsafe {} does not disable type checks or ownership rules. It (basically) only allows dereferencing a raw pointer. It's a way of telling the compiler "in this specific place, for this specific variable I manually verified correctness of dereferencing that pointer". C/C++ is not just "everything is unsafe", it's also lacking many powerful features that exist even inside unsafe{} blocks: ergonomic enums with associated data, type-checked generics, ergonomic moves etc.
- pizza234 5y agoFirst, there's a misconception to be cleared :) Unsafe code is not the only solution to borrow checking problems, as Rust supports reference counting, which is an implementation of shared ownership. Whether this defeats the point of Rust is subjective, but regardless, it's a solution to at least certain ownership problems. A very typical area where you can observe this is bidirectional trees. They're ugly in Rust! :) Now, memory unsafety is not a black and white matter. In my experience with the community, you'll be surprised at how much pragmatism there is. Nobody will scream sacrilege if one expresses the necessity to use memory unsafe code¹. The connection to your question is that overally, the Rust memory safety model is not all-or-nothing, rather, a spectrum of safety above the convenientional entirely-unsafe approach. With this in mind, I'd say that "98% Rust-safety" is well beyond the "100% unsafe" approach of traditional low-level languages. As a matter of fact, I suppose that in the future, we'll see languages developing intermediate memory safety features (I think Zig was doing something like that). [¹] The actix-web case is a very interesting one. I haven't looked at it, but my personal suspicion is that it made use of _wildly_ unsafe code, and that usafe of unsafe code was not the problem, rather that unnecessary usage was.
- cies 5y ago> The actix-web case IIRC it was mostly the initial reluctance of the author to merge PRs that rewrote unsafe to safe Rust. Some escalation, some emotions, and then everything moved forward in harmony and... with more safety.
- nuerow 5y ago> IIRC it was mostly the initial reluctance of the author to merge PRs that rewrote unsafe to safe Rust. No, not really. Spinning the actix-web as the author's reluctance to accept PRs is a gross misrepresentation of the problem faced by the project. The author even went to great strides to explain that his use of unsafe code was indeed safe. For context, here's what the author had to say about that regrettable episode. https://github.com/fafhrd91/actix-web-postmortem https://github.com/fafhrd91/actix-web-postmortem
- 5y ago
- Patix0331 5y agoCopy paste from https://this-week-in-rust.org/blog/2021/10/06/this-week-in-rust-411/#quote-of-the-week https://this-week-in-rust.org/blog/2021/10/06/this-week-in-r... There's a common trope among people unfamiliar with rust where they assume that if you use unsafe at all, then it's just as unsafe as C and rust provided no benefit. Comparing C's approach to safety vs Rust's is like comparing an open world assumption to a closed world assumption in formal logic systems. In C, you publish your api if it's possible to use correctly (open world). In Rust, you publish a safe api if it's im possible to use in correctly (closed world). Rust's key innovation here is that it enables you to build a 'bridge' from open world (unsafe) to a closed world (safe), a seemingly impossible feat that feels like somehow pairwise reducing an uncountable infinity with a countable infinity. Rust's decision to design an analogous closed-world assumption for safe code is extremely powerful, but it seems very hard for old school C programmers to wrap their head around it.
- sgift 5y agoInteresting enough Java has the same question. There is a sun.misc.Unsafe class (https://blogs.oracle.com/javamagazine/post/the-unsafe-class-unsafe-at-any-speed https://blogs.oracle.com/javamagazine/post/the-unsafe-class-...) which is used internally to provide the safe code you can use in normal Java code. This is needed since at some point you will always have to go down to something which a machine cannot verify (e.g. "this port will hold the result provided by an external machine and the machine operator guarantees me it will always be a non-null address) and the guideline is the same as in rust: If you have to use this, you are responsible to hold all invariants and if you do this and therefore "guarantee" (usually it's done by tests, so it's not really guaranteed, but you get the idea) people using the safe abstractions can be sure that their code will work. My point being: Unsafe does not invalidate the safety of the overall code, it shows you places where you have to be extra carefully. On the other hand if you yourself do not use unsafe and all the libraries you use are of high quality you can be sure your code will be correct. Which is the best you can do in reality.
- fiedzia 5y agoTwo things here: 1. Restricting unsafe to limited number of places is a lot better than having it as a default everywhere. 2. The idea is that if you have to use unsafe, you (or some other module author) would wrap it in an interface that is safe to use. Overall this may not be perfect (in the sense of "its 100% verified by compiler") and finding bugs does happen in supposedly safe abstractions over unsafe, but Rust moves this from "its a norm" to "its very, very rare". 3. Unsafe lifts many restrictions, but not all of them, so there still is some validation. Keep in mind that GC and a large parts of low-level code that is fundamental for languages that have GC is written in C, and we still are findings safety bugs there.
- volta83 5y agoUsing unsafe is like providing a "proof" that the code you write won't cause safe Rust to exhibit undefined behavior. Writing unsafe requires a certain degree of skill, because one needs a deep understanding about what guarantees safe Rust provides, to ensure that your abstraction does not break them. If you read the standard library docs, every unsafe block has a comment explaining / proving why it can't cause safe Rust code to exhibit undefined behavior. Many of these blocks, particularly those in libcore, have been proved formally correct in proof assistants. There is a theorem that proves that "if a safe Rust wrapper over unsafe code is proven correct, then extending safe Rust with that wrapper is still sound". So you basically can infinitely extend the safe Rust subset of Rust with these abstractions. > But if “unsafe” is used, can we still claim that Rust code is safe? So the answer to this is "no, you can't claim that, you have to actually go and prove it". Often, very often actually, the proofs are trivial. For example, to index into a slice: fn index_slice(slice: &[T], i: usize) // SAFETY: if the index is not in bounds, we panic assert!(i < slice.len()); unsafe { *slice.as_ptr().add(i) } } suffices. Why? Because code that creates a slice with a ptr and a len field that do not point to a valid allocation with len valid elements already exhibits UB. That is, for any valid Rust program, we are guaranteed here that these two fields are "ok". So the only thing we need to make sure of is that the index "i" is inbounds, and that's trivial to do with an assert that panics if this is not the case. That is, at runtime, no program for which the index is not inbounds will reach the ptr.add(i) method, so there is no way to offset this pointer out of bounds, much less dereference it. Basically, because all safe Rust code can rely on all other safe Rust code upholding the rules, most of the proofs are really easy. The fact that you can prove safe abstractions over unsafe code in isolation from other abstractions is one of the main features of Rust. --- For synchronization primitives, proving the absence of data-races is particularly hard, and requires a lot of expertise. The subset of Rust programmers that actually need to do this is very small. Most people just use those primitives that have already been proven today, of which there are many.
- nuerow 5y ago> Using unsafe is like providing a "proof" that the code you write won't cause safe Rust code to exhibit undefined behavior. My take is that Rust is proposed based on the value proposition of its Safe Rust feature, but in the Linux kernel that feature has limited use. Therefore, if Safe Rust is out of the table then what's the point of writing/rewriting parts of the Linux kernel in Rust? What's the value proposition?
- tialaramex 5y agoIn respect of ownership models the borrow checker doesn't natively understand specifically, the Rust standard library provides a bunch of obvious things you would want, such as a Reference Counted type and the idea of Weak references. These are of course unsafe internally, but assuming you believe they are correctly implemented, it's safe to use them and so Rust code which uses Rc or Arc (an atomic reference counted type) is safe. Now, beyond obvious things a beginner would want, you start to get into some pretty cool structures, and many of those are not provided by Rust's standard library, however, if you aren't excited about implementing them yourself others have likely done so. For example maybe you've read about "Hazard pointers" for very concurrent systems where you can't afford to take a lock when destroying shared resources but you want to only destroy resources that are no longer used. Rust doesn't provide Hazard pointers in its standard library, but several crates offer different takes on this. Again, the implementations are unsafe internally, but if you trust that the programmer got them right, your use of their library is safe.
- honkycat 5y agoThey have a data structure that will do reference counting just like what most garbage collection systems use. You see it used every so often.
- Const-me 5y agoRef.counting is slow, causes too many extra memory requests. Also, when the program is multithreaded, access to cache lines recently modified by another CPU core is expensive, even more so that a cache miss. For these reasons, most garbage collection systems are using mark and sweep, or something similar.
- amrx101 5y agoReference counting man. When the programs ownership model doesn't adhere to Rust's borrow checker.