12 ms·
I thought this was going to be a response to Jonathan Blow's video about how doing your own memory management is effectively turning off the borrow checker: htt
by kbwt 8y ago
I thought this was going to be a response to Jonathan Blow's video about how doing your own memory management is effectively turning off the borrow checker: https://www.youtube.com/watch?v=4t1K66dMhWk https://www.youtube.com/watch?v=4t1K66dMhWk
The takeaway being that the borrow checker doesn't magically prevent the use-after-free class of bugs. Although you will never experience a segmentation fault in safe Rust, the bug is still there and your program keeps running in an invalid state. The symptoms are changed, but no less dangerous.
To make the problem even more obvious, think of allocating a large array to be used as a heap and handing out indices to implement your own malloc. You have bounds checking to prevent indexing outside the bounds of the heap, but it doesn't really help when the elements have logically different lifetimes and occupy different parts of the array. I don't think this is a contrived example either. A less obvious version of this can easily creep into large or complex systems, as evidenced by the Entity Component System in Rust example.
- rectang 8y agoWhat I'd like to understand better is what idioms are emerging as best practices thanks to the (good!) pressure that Rust puts on us. For memory management, some of the major algorithms available are global allocation, stack allocation, malloc/free, reference counting, and tracing garbage collection. (The Entity Component System model is doing your own allocation so it's a subtype of either malloc/free or ref counting.) Stack allocation and global allocation with borrow checking seem very attractive in terms of safety, but you have to know more in advance about how the memory is going to be used because growing the allocation while you are deeper in the stack is not possible. Are there any idioms or algorithms which are particularly friendly to this model? For instance, traversing a preliminary object graph to establish max allocation requirements before allocating a large block on the stack?
- steveklabnik 8y ago> What I'd like to understand better is what idioms are emerging as best practices thanks to the (good!) pressure that Rust puts on us. We all would. They're still emerging! I think the generational index idea is one of them, though.
- sdegutis 8y agoSo it’s the difference between crash early (C) and don’t crash but run wrong (Rust), inherent by design in the main selling point of Rust (borrow checker)?
- Tuna-Fish 8y agoNo. Most modern C++ ECS take the same approach as the Rust ECS that is being discussed. Also, since I started using generational indexes, I have never had a bug caused by using stale data, so at least for me it hasn't been a real issue.
- andrewflnr 8y agoBroadly speaking, C is the king of running wrong without crashing, so no. The borrow checker isn't really making anything worse, it's just failing to save you from this particular problem. In C you could still write the exact same bug, or you could even write it with actual pointers. I gather a lot of browser exploits start that way these days.
- gliptic 8y agoWhy is everyone assuming C is "crash early" as opposed to "undefined behaviour will crash if you're lucky and cause RCE in the worst case, or any weird thing in between". The selling point of the borrow checker (at least one of them) is that it doesn't allow undefined behaviour unless you explicitly enable unsafe operations. If there was a way to detect UB and predictably crash in a performant way, you could probably implement that in Rust as well. In fact, that's often what is done with generational indexes and similar.
- shawn 8y agohttps://doc.rust-lang.org/nomicon/print.html https://doc.rust-lang.org/nomicon/print.html Rust is otherwise quite permissive with respect to other dubious operations. Rust considers it "safe" to: Deadlock Have a race condition Leak memory Fail to call destructors Overflow integers Abort the program Delete the production database However any program that actually manages to do such a thing is probably incorrect. Rust provides lots of tools to make these things rare, but these problems are considered impractical to categorically prevent.
- zrm 8y ago> The symptoms are changed, but no less dangerous. Potentially even more dangerous. Having the program segfault immediately is much preferable to, say, Heartbleed.
- barrkel 8y agoIf only we could guarantee immediate segfaults.
- kibwen 8y agoThat's literally what Rust compiler errors are doing. :P
- zrm 8y ago> If only we could guarantee immediate segfaults. It should be possible to create a malloc implementation that does that by making the minimum allocation size a page and then not reusing virtual addresses for new allocations. Then once an allocation is freed, any access to it is permanently a segfault. That may not be practical on existing architectures with 48-bit virtual addressing though, since you could plausibly exhaust the address space. The full 64 bits might be sufficient for most things at least. You could also get most of the benefit by not reusing virtual addresses until you run out.
- steveklabnik 8y agoIt's not, because that relies on actually doing the de-reference. Thanks to UB, that may never actually happen, the code may get removed entirely.
- zrm 8y agoIf the code is removed entirely then what memory is being improperly accessed?
- steveklabnik 8y ago
- sidlls 8y agoI think the primary argument for Rust advocates here is that what you describe is only possible by using explicitly "unsafe" code (i.e. code using the "unsafe" block that is required). The weakness of this argument is exposed in your comment also: in libraries "unsafe" is often hidden behind an abstraction. To take a simple example, consider the standard Rust Vec: plenty of "unsafe" code resides in this object, but you'll rarely if ever see "unsafe" wrap typical vector operations ("unsafe { myvec[0] }"). Partial mitigation is that this does reduce the surface area where one might have to look if odd behaviors start to appear in a Rust program. Overall it's still a net benefit.
- steveklabnik 8y ago> ("unsafe { myvec[0] }") Part of the point in my post is that this does not remove the bounds check. This unsafe block does nothing. This actually might be a better example than the one I picked...
- kibwen 8y agoAnd just as in the OP, the compiler makes it clear that this unsafe block does nothing: warning: unnecessary `unsafe` block --> src/main.rs:4:3 | 4 | unsafe { myvec[0]; } | ^^^^^^ unnecessary `unsafe` block |
- saghm 8y agoI might be in the minority here (and I don't do game development, which is the topic of this thread), but I tend to start by using `vec.get(i)` (which returns an option) and then only switch to the index notation if I end up checking the length immediately beforehand and want to do an early return or something to avoid extra indentation in the code where I use the accessed element.
- majewsky 8y agoI think modern C++ compilers are smart enough to eliminate the bounds check if they can prove that it never fails.
- steveklabnik 8y agoSo, I had been thinking about this post for a while, and Blow's video caused some more discussion that made me post it. But it's not a direct response, I still haven't watched the video, and so I don't know what he actually said. If I wanted it to be a response, I would have linked to it. > The symptoms are changed, but no less dangerous. I would take issue with this sentiment. There's a world of difference between "logic error and/or panic" and "undefined behavior". Yes, Rust doesn't fix all bugs. But it's still an improvement here.
- kbwt 8y agoThanks for the response. I wasn't trying to speculate on your intentions for publishing the blog post. > There's a world of difference between "logic error and/or panic" and "undefined behavior". Is is really so different for the programmer who wrote the bug? If you have undefined behavior, the language implementation can do whatever it wants. It won't actively work against you, but the implementer is given permission to ignore what would happen if you violate their assumptions. With a logic error in custom memory management, the program execution will still be following well-defined rules but the invariants assumed by the programmer will no longer hold. The resulting behavior appears effectively undefined to the programmer, because the point of invariants is to ignore what would happen when they are broken. Defensive coding with panics/asserts will definitely help catch some of these mistakes during development. > Yes, Rust doesn't fix all bugs. But it's still an improvement here. I applaud your efforts with Rust, it's great to see someone actually trying to improve the state of programming languages.
- steveklabnik 8y agoIt’s all good, it’s a totally reasonable thing, which was also brought up in all the other threads :) > it won’t actively work against you I guess it depends on what you mean by “active.” Consider the Option<NonNull<T>> case. We can do the null check in safe Rust. We know the check is done. Now consider the case with UB: https://blogs.msdn.microsoft.com/oldnewthing/20140627-00/?p=633 https://blogs.msdn.microsoft.com/oldnewthing/20140627-00/?p=... These kinds of things can cause lots of subtle issues. The rust code won’t.
- 8y ago
- pcwalton 8y ago> The symptoms are changed, but no less dangerous. The symptoms of a use-after-free-style logic error are less dangerous in Rust, because it's much harder to get RCE.