7 ms·
I’m not very well versed in kernel development. But I am a Rust dev and have observed the discussion about Rust in Linux with interest… Having said that, this p
by smodo 2y ago
I’m not very well versed in kernel development. But I am a Rust dev and have observed the discussion about Rust in Linux with interest… Having said that, this part of the article has me baffled:
>> implementing these features for a smart-pointer type with a malicious or broken Deref (the trait that lets a programmer dereference a value) implementation could break the guarantees Rust relies on to determine when objects can be moved in memory. (…) [In] keeping with Rust's commitment to ensuring safe code cannot cause memory-safety problems, the RFC also requires programmers to use unsafe (specifically, implementing an unsafe marker trait) as a promise that they've read the relevant documentation and are not going to break Pin.
To the uninformed this seems like crossing the very boundary that you wanted Rust to uphold? Yes it’s only an impl Trait but still… I can hear the C devs now. ‘We pinky promise to clean up after our mallocs too!’
- foundry27 2y agoRust’s whole premise of guaranteed memory safety through compiletime checks has always been undermined when confronted with the reality that certain foundational operations must still be implemented using unsafe. Inevitably folks concede that lower level libraries will have these unsafe blocks and still expect higher level code to trust them, and at that point we’ve essentially recreated the core paradigm of C: trust in the programmer’s diligence. Yeah Rust makes this trust visible, but it doesn’t actually eliminate it in “hard” code. The punchline here, so to speak, is that for all Rust’s claims to revolutionize safety, it simply(!) formalizes the same unwritten social contract C developers have been meandering along with for decades. The uniqueness boils down to “we still trust the devs, but at least now we’ve made them swear on it in writing”.
- stackghost 2y agoI'm not a rust fanboy but isn't the point of rust to dramatically decrease the area in which null pointer dereferences and friends can occur, and thus make them more likely to be spotted?
- steveklabnik 2y agoNot just spotted, but easier to find after the fact when the problematic behavior happens.
- wbl 2y agoThe difference is every line of C can do something wrong while very few lines of Rust can. It's much easier to scrutinize a small well contained class with tools like formal methods than a sprawling codebase.
- uecker 2y agoIf you limited wrong to "memory safe" and also ignore that unsafe parts violating invariants can make safe parts of Rust to be wrong.
- Dylan16807 2y ago> If you limited wrong to "memory safe" Yes, because this is a discussion about the value of "unsafe", so we're only talking about the wrongs that are enabled by "unsafe". > and also ignore that unsafe parts violating invariants can make safe parts of Rust to be wrong. If I run a line of code that corrupts memory, and the program crashes 400 lines later, I don't say the spot where it crashes is wrong, I say the memory corrupting line is wrong. So I disagree with you here.
- uecker 2y agoIt does not invalidate an argument that you do not want to talk about it. Regarding the second point: yes, you can then blame the "unsafe" part but the issue is that the problem might not be so localized as the notion of "only auditing unsafe blocks is sufficient" implies. You may need to understand the subtle interaction of unsafe blocks with the rest of the program.
- dwattttt 2y ago> the problem might not be so localized as the notion of "only auditing unsafe blocks is sufficient" implies It depends on what you consider "problem" can mean. An unsafe function needs someone to write unsafe in order to call it, and it's on that calling code to make sure the conditions needed to call the unsafe function are met. If that function itself is safe, but still let's you trigger the unsafe function unsafely? That function, which had to write 'unsafe', has a bug: either it's not upholding the preconditions of the unsafe function it's calling, or it _can't_ uphold the preconditions without their own callers also being in on it, in which case they themselves need to be an unsafe function (and consider whether their design is a good one). In this way, you'll always find unsafe 'near' the bug.
- kelnos 2y agoI don't think you're giving Rust enough credit here. For those projects that don't use any unsafe, we can say -- absent compiler bugs or type system unsoundness -- that there will be no memory leaks or data races or undefined behavior. That's useful! Very useful! For projects that do need unsafe, that unsafe code can be cordoned off into a corner where it can be made as small as possible, and can be audited. The rest of the code base is just as safe as one with no unsafe at all. This is also very useful! Now, sure, if most projects needed to use unsafe, and/or if most projects had to write a significant amount of unsafe, then sure, I'd agree with you. But that's just not the reality for nearly all projects. With C, everything is unsafe. Everything can have memory leaks or data races or undefined behavior. Audits for these issues need to examine every single line of code. Compilers and linters and sanitizers can help you here, but they can never be comprehensive or guarantee the absence of problems. I've been writing C for more than 20 years now. I still write memory leaks. I still write NULL pointer dereferences. I still struggle sometimes to get my data ownership (and/or locking) right when I have to write multithreaded code. When I get to write Rust, I'm so happy that I don't have to worry about those things, or spend time with valgrind or ASAN or clang's scan-build to figure out what I've done wrong. Rust lets me focus more on what I actually care about, the actual code and algorithms and structure of my program.
- dzaima 2y agonit - Rust does allow memory leaks in safe code. https://doc.rust-lang.org/std/mem/fn.forget.html#safety https://doc.rust-lang.org/std/mem/fn.forget.html#safety
- eru 2y agoYes, memory leaks are rarer in Rust than in C, but they are an entirely different topic that 'unsafe' blocks.
- thayne 2y agoIt's also possible to leak memory in languages with tracing garbage collection, just create a data structure that holds strong references to objects that are no longer needed, which commonly happens when using something like a HashMap as a cache without any kind of expiration.
- jchw 2y agoI think when people come to these conclusions it's largely due to a misunderstanding of what exactly the point of most programming language safety measures are and why they make sense. Something that people often ponder is why you can't just solve the null safety problem by forcing every pointer dereference to be checked, with no other changes. Well of course, you can do that. But actually, simply checking to make sure the pointer is non-null at the point of dereference gets you surprisingly little. When you do this, what you're (ostencibly) trying to do is reduce the number of null pointer dereferences, but in practice what happens now is that you just have to explicitly handle them. But, in a lot of cases, there's really nothing particularly sensible to do: the pointer not being null is an invariant that was supposed to be upheld and it wasn't, and now at the point of dereference, at runtime, there's nothing to do except crash. Which is what would've happened anyways, so what's the point? What you really want to do isn't actually prevent null pointer dereferences, it's to uphold the invariants that the pointer is non-null in the first place, ideally before you leave compile time. Disallowing "unsafe" operations without marking them explicitly unsafe doesn't give you a whole lot, but what you can do is expand the number of explicitly safe operations to cover more of what you want to do. How Rust, and many other programming languages, have been accomplishing this is by expanding the type system, and combining this with control flow analysis. Lifetimes in Rust are a prime example, but there are many more such examples. Nullability, for example, in languages like TypeScript. When you do it this way, the safety of such "safe" operations can be guaranteed, and while these guarantees do have some caveats, they are very strong to a lot of different situations that human code reviews are not, such as an unsafe combination of two otherwise-safe changesets. It's actually totally fine that some code will probably remain unable to be easily statically verified, the point is that we want to reduce the amount of code that can't be easily statically verified to be as small as possible. In the future we can use much less easy approaches to statically verify unsafe blocks, such as using theorem provers to try to prove the correctness of "unsafe" code. But even just reducing the amount of not-necessarily-memory-safe code is an enormous win, for obvious reasons: it dramatically reduces the surface area for vulnerabilities. Moreover, time and time again, it is validated that most new vulnerabilities come from relatively recent changes in code, which is another huge win: a lot of the unsafe foundations actually don't need to be changed very often. There is absolutely nothing special about code written in Rust, it's doing the same shit that C code has been doing for decades (well, on the abstract anyway; I'm not trying to downplay how much more expressive it is by any means). What Rust mainly offers is a significantly more advanced type system that allows validating many more invariants at compile-time. God knows C developers on large projects like the Linux kernel care about validating invariants: large amounts of effort have been poured into static checking tools for C that do exactly this. Rust is a step further though, as the safe subset of Rust provides guarantees that you basically can't just tack onto C with only more static checking tools.
- Rusky 2y agoIt's not the same unwritten social contract: in Rust even the unsafe code has the same stricter type signatures as the safe code, so there is a formal way to judge which part of the program is at fault when the contract is broken. You might say the contract is now written. :-) In C, the type system does not express things like pointer validity, so you have to consider the system as a whole every time something goes wrong. In Rust, because the type system is sound, you can consider each part of the program in isolation, and know that the type system will prevent their composition from introducing any memory safety problems. This has major implications in the other direction as well: soundness means that unsafe code can be given a type signature that prevents its clients from using it incorrectly. This means the set of things the compiler can verify can be extended by libraries. The actual practice of writing memory-safe C vs memory-safe Rust is qualitatively different.
- im3w1l 2y ago> In Rust, because the type system is sound Unfortunately, it's not. Now I do think it will be eventually fixed, but given how long it has taken it must be thorny. https://github.com/rust-lang/rust/issues/25860 https://github.com/rust-lang/rust/issues/25860
- simonask 2y agoIn practice this is a compiler bug, though, and is treated as such, and not a soundness hole in the abstract design of the type system. There has also not been a single case of this bug being triggered in the wild by accident.
- _flux 2y agoI take this to mean e.g. the Oxide project has proven the Rust type system sound? There was a Git repository demontrating unsoundness issues in the compiler, but I seem to be unable to find it anymore :/. It seemed like there would be more than one underlying issue, but I can't really remember that.
- 2y ago
- thfuran 2y agoWould you similarly say that Russian Roulette is the same game whether the revolver has two chambers or ten thousand?
- sqeaky 2y agoConstrained and minimized trust in programmer diligence is better than unconstrained and omnipresent trust in the same.
- Ar-Curunir 2y agoThis is nonsense. Just because some small parts of the code are must be annotated as unsafe doesn’t mean that we’re suddenly back to C land. in comparison, with C the entire codebase is basically wrapped in a big unsafe. That difference is important, because in Rust you can focus your auditing and formal verification efforts on just those small unsafe blocks, whereas with C everything requires that same attention. Furthermore, Rust doesn’t turn off all checks in unsafe, only certain ones. ,
- uecker 2y agoIf you think correctness is only about memory safety, only then you can you can "focus your auditing and formal efforts on just those small unsafe blocks". And this is a core problem of Rust that people think they can do this.
- erik_seaberg 2y agoMemory and concurrency safety need to be the first steps, because how can you analyze results when the computer might not have executed your code correctly as written?
- Ar-Curunir 2y agomy comment (and indeed in this entire comment chain) is within the context of memory safety. This should have been clear because of the focus on unsafe, which, compared to normal Rust, relaxes only memory safety. Obviously if you want to get formal guarantees beyond that property, you have to reason about safe code also. (Also, the comparison in this entire chain is against C, and the latter is better than Rust in this regard… how?)
- uecker 2y agoYes, this discussion is about memory safety, but this does not invalidate my argument. There is no point in only auditing your code with respect to memory safety, so the argument that you can simply ignore everything outside "unsafe" blocks is simply wrong.
- mort96 2y agoIt is literally impossible to build systems where you never at any point trust that underlying systems work correctly. This is a boring and uninformative criticism. It is the case for every language ever invented.
- pornel 2y agoBuilding safe abstraction around unsafe code works, because it reduces the scope of the code that has to be reviewed for memory safety issues. Instead of the whole codebase being suspect, and hunting for unsafety being like a million-line "Where's Waldo?", it reduces the problem to just verifying the `unsafe` blocks against safety of their public interface, "is this a Waldo?". This can still be tricky, but it has proven to be a more tractable problem.
- deleted 2y ago[deleted]
- gpm 2y agoThis kind of use of unsafe has been in rust forever, for example with `Sync`, `Send`. Implementing an unsafe marker trait to promise to the compiler that other methods on the structure act a certain way. The scope of an unsafe block has always been interpreted to include guaranteeing things about it's surroundings up to other things in the same module. E.g. if I'm implementing `Vector::push` I'm going to rely on the fact that `self.capacity` really is the size of the allocation behind `self.ptr` without verifying it, and I'm going to feel good about that because those fields aren't public and everything within the module doesn't let you violate that constraint, so it's not possible for external safe code to violate it. The same applies to marker traits. If I'm writing `unsafe impl Send for MyStruct {}` I'm promising that the module exposes an interface where `MyStruct` with will always comply with the requirements of `Send` no matter what safe external code does (i.e. sending MyStructs across threads is safe given the exposed API). With this proposal if I write `unsafe impl PinCoerceUnsized for MyStruct {}` I'm promising the same with respect to that (whatever the documentation for that trait ends up saying, which should essentially be that I've implemented `Deref for MyStruct` in the same module and I don't expose any way for safe external code to change what reference `Deref` returns).
- mise_en_place 2y agoI'm not convinced you can have your cake and eat it too. Not a dig at Rust specifically; it's more like, you can have managed memory, or manually manage it yourself. There is no in-between. The implementation ends up being a kludge. Terry had it right, just put everything in the lower 2 GB and DMA it.
- remram 2y agoI think the part that uses `unsafe` and can break Pin if done wrong is the implementation of the smart-pointer type, not its use.
- GolDDranks 2y agoTry imagining trait AlwaysIndexableUnder100. There's a generic codebase/library that takes in types that implement that trait, and do indexing with indexes that are always below 100. Like `usersCustomSliceA[4] = usersCustomSliceB[5];` You'd be tempted, for performance, to use `get_unchecked` methods that skip the boundary checks. After all, the trait says that this should always succeed. However, if the user passes in a type that is not indexable with integers smaller than 100, whose fault it is if the program segfaults? The users? But they managed to get the program to segfault _without_ writing unsafe code. The provider of the library? They are using `unsafe` to call `get_unchecked` after all. Bingo. It's the library dev's fault. The API they provide is not sound. However, they can make it sound by marking the _trait_ unsafe to implement. Then the user needs to type `unsafe` when implementing the trait for their type. "I solemnly swear that this type is actually indexable with all integers smaller than 100." That shifts the blame to the mistaken implementation, and the user is to blame. It's the same situation here. Deref is not unsafe to implement. That's why if you need to uphold a trust boundary, you need an unsafe trait. So, the whole thing doesn't account to crossing the boundary willy nilly, a big point of Rust's unsafe is for the compiler to force documenting and checking for accountability: who is required to do what, and who is allowed to rely on that.
- rocqua 2y agoRust is all about ring-fencing the scary parts in unsafe. A rust program that doesn't use unsafe, and only uses dependencies that are sound with respect to unsafe, is guaranteed to be fine. And it is very easy to write code without using unsafe. Unlike C, where code style that is guaranteed to be memory safe is nigh impossible. The difficult bit with Rust is still the sound use of unsafe, but it is quite feasible to do that by hand. It does, sadly, require looking at the entire module that contains the unsafe code.