3 ms·
I think the generalization of "use-after-free" to "use-after-invalidation" is the most important part of this article, showing that it's more about object "life
by moosingin3space 9y ago
I think the generalization of "use-after-free" to "use-after-invalidation" is the most important part of this article, showing that it's more about object "lifetimes" than simply a memory management flaw. This is one reason why we can't consider "modern C++" to be memory-safe.
I wonder if this sort of vulnerability is possible in Rust, and if it is, is there a way to use Rust's borrow checker to make it impossible?
- richardwhiuk 9y agoI can't imagine a way in which you could make that code compile in Rust, without using unsafe.
- tyoverby 9y ago> I think the generalization of "use-after-free" to "use-after-invalidation" is the most important > I wonder if this sort of vulnerability is possible in Rust Rusts borrow checker is designed for the more general case of "use-after-invalidation" and `free` is treated as a simple invalidation of what happens to be a heap allocated structure. Interestingly, the borrow checker also prevents invalidations that are still common in memory-safe languages such as iterator invalidation.
- petertodd 9y agoWith good design, the range of invalidations that Rust can prevent go well beyond memory invalidation. For example, a serialization/deserialization library I'm working on has the following trait: pub trait Decode : Sized { fn decode<D: Decoder>(decoder: D) -> Result<(Self, D::Done), D::Error>; } Basically, types that can be decoded implement this trait. The trick here is that the decode() method consumes the decoder, and then returns it in the output. In the case of decoding an IO stream, the D::Done type is the IO stream itself, which means that in the event of an error, we ensure that the user can't accidentally use the IO stream again in an incompletely decoded state because all they have is the error type, D::Error (they can intentionally use the IO stream again by recovering the IO stream handle from the D::Error type). In practice, the above results in decode() implementations that look like the following: fn decode<D: Decoder>(decoder: D) -> Result<(Foo, D::Done), D::Error> { let (v0, decoder) = decoder.decode()?; let (v1, decoder) = decoder.decode()?; let (v2, decoder) = decoder.decode()?; Ok((Foo(v0, v1, v2), decoder.done()?)) } This is a bit more verbose, but as I also make use of Rust's procedural macros you'd also never actually write the above code; it's auto-derived/auto-generated for you 99% of the time. Equally, if I ever do make a mistake in the auto-generation this state-machine-like approach makes it very likely that the resulting auto-generated code won't even compile.
- cousin_it 9y agoWhat? It's very easy to get use-after-invalidation in Rust. Destructors called during unwinding see stuff in an invalid state. You can probably make a language that prevents use-after-invalidation in safe code (e.g. mark all accessible mutable references as "dirty" during unwinding, and require unsafe code to "clean" them) but Rust isn't trying to do that AFAIK.
- shepmaster 9y agoCould you provide an example of such code? I was under the impression that certain things were disallowed because destructors aren't allowed to see the struct in an invalid state. A common case where I see people trying to do this is when you have a struct where you are trying to replace a member variable: struct Foo { thing: Vec<i32>, } impl Foo { fn something(&mut self) -> Vec<i32> { let temp = self.thing; // If we panicked between these two lines, then the struct would be in an undefined state self.thing = vec![1]; temp } } This code produces the error `cannot move out of borrowed content`. For those curious, you normally would write this as use std::mem; impl Foo { fn something(&mut self) -> Vec<i32> { mem::replace(&mut self.thing, vec![1]) } }
- deleted 9y ago[deleted]
- cousin_it 9y agoHow about this? struct Foo { bar: Bar } struct Bar { message: &'static str } fn change_foo(foo: &mut Foo) { change_bar(&mut foo.bar); } fn change_bar(bar: &mut Bar) { bar.message = "Invalid"; if true { panic!(); } bar.message = "Valid"; } impl Drop for Foo { fn drop(&mut self) { println!("{}", self.bar.message); } } fn main() { let mut foo = Foo { bar: Bar { message: "" } }; change_foo(&mut foo); } The destructor of a struct sees a broken invariant of a nested struct.
- 9y ago
- Animats 9y agoIn this example, though, the library's C++ sort is calling out to Javascript code during the sort. That code can alter the object being sorted. That's where the trouble comes from. The sort function needs exclusive mutable access to the object being sorted. But Javascript doesn't support such access control. This is a general problem with cross-language data access. The languages may not have the same data model. It's especially bad when one side has garbage collection, and the other side has to have explicit GC-aware code.