Ode to the use-after-free: one vulnerable function, a thousand possibilities(scarybeastsecurity.blogspot.com)
scarybeastsecurity.blogspot.com
Ode to the use-after-free: one vulnerable function, a thousand possibilities
https://scarybeastsecurity.blogspot.com/2017/05/ode-to-use-after-free-one-vulnerable.html
26 comments
> 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.
> 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.
With 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:
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:
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.In 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.
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.
What? 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.
Could 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:
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]
How 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.I see, I think you are using a different definition of "invalid" than I and the grandparent are. Rust will not allow you to access memory that is invalid, but your own invariants can certainly be broken.
For what it's worth, the solution I've seen for this type of case is another struct that is used to restore to an acceptable state:
For what it's worth, the solution I've seen for this type of case is another struct that is used to restore to an acceptable state:
fn change_bar(bar: &mut Bar) {
let mut restore = Restore(bar);
restore.message = "Invalid";
if true {
panic!();
}
restore.message = "Valid";
// Don't rollback on success
std::mem::forget(restore);
}
struct Restore<'a>(&'a mut Bar);
// Put back to an acceptable state
impl<'a> Drop for Restore<'a> {
fn drop(&mut self) {
self.0.message = "Restored to some state";
}
}
// Sugar so we don't have to know about the wrapper
impl<'a> std::ops::Deref for Restore<'a> {
type Target = Bar;
fn deref(&self) -> &Bar { self.0 }
}
// Sugar so we don't have to know about the wrapper
impl<'a> std::ops::DerefMut for Restore<'a> {
fn deref_mut(&mut self) -> &mut Bar { self.0 }
}I can't imagine a way in which you could make that code compile in Rust, without using unsafe.
A simple way to approach this problem in C, that will work in most cases, is something like this:
void safer_free(void **x) {
if(x!=NULL && *x!=NULL) {
free(*x);
}
*x = NULL;
}
the key part being to set the original pointer to NULL so a future use will cause an exception, not a vulnerability.Minor pedantic note: the *x!=NULL check is unnecessary. Calling free(NULL) is specified to do nothing.
Except this won't work when multiple pointers exist to the same data.. which is arguably when this kind of stuff is most difficult
If you have multiple pointers to the same data with different lifetimes you already need some kind of reference counting or garbage collection.
That's why "safer" :)
It works in the vast majority of cases. In the rare other cases, you should spend some extra thought on your pointer usage to make sure you have a set of abstractions that let a human reason about the memory sensibly (either that or an insane number of test cases to catch any problem). Someone else mentioned reference counting, and that can be a good idea.
The bottom line is you need to do something to make sure you're not doing use-after-free, and it's mostly not difficult.
It works in the vast majority of cases. In the rare other cases, you should spend some extra thought on your pointer usage to make sure you have a set of abstractions that let a human reason about the memory sensibly (either that or an insane number of test cases to catch any problem). Someone else mentioned reference counting, and that can be a good idea.
The bottom line is you need to do something to make sure you're not doing use-after-free, and it's mostly not difficult.
It works on most cases that people normally get right, and is useless on most cases that people normally get wrong.
Overall, it's not useless, but it's really not worth modifying the language for it.
Overall, it's not useless, but it's really not worth modifying the language for it.
I was just going to post a comment asking why your proposed way is not the standard way. Surely free is (usually) not called that often that the overhead would be anything close to unbearable? This seems like a so simple method of handling most of the cases, as you point out, that I feel I'm missing some obvious downside.
There is no generic pointer-to-pointer in C. You get an incompatible-pointer-types warning in gcc when trying to use void * * : https://godbolt.org/g/vxWFvl
There is probably scope for use of C11 _Generic, but I don't think you could support user-defined types in that way.
The usual macro problems apply when trying to do it as a macro:
There is probably scope for use of C11 _Generic, but I don't think you could support user-defined types in that way.
The usual macro problems apply when trying to do it as a macro:
#define FREE(X) (free(X),(X)=NULL,(void)0)
In C++ you can do something like this, which works well enough: (I don't use this specifically myself, but I have something similar for COM objects) template<class T>
static void Free(T *&p) {
free(p);
p=nullptr;
}
This isn't completely useless, but it's not worth sweating blood over. Personally I think I've had more mileage from good-quality debug functionality in the memory allocator: fill newly-allocated and newly-freed regions with junk; check junk and metadata periodically; unmap regions promptly; and so on.When the language was made 40 years ago, the extra overhead was unbearable.
Or rather, more likely, they were so focused on efficiency (because that's what you do when you have a 1Mhz computer), that the idea of making things easier for people in this way didn't cross their mind.
Or rather, more likely, they were so focused on efficiency (because that's what you do when you have a 1Mhz computer), that the idea of making things easier for people in this way didn't cross their mind.
Apparently this myth never dies.
Burroughs did not have this issue with ESPOL and NEWP almost 10 years earlier before C was born.
Nor did the UK Navy with Algol 68RS or the multiple institutes using PL/I variants.
Burroughs did not have this issue with ESPOL and NEWP almost 10 years earlier before C was born.
Nor did the UK Navy with Algol 68RS or the multiple institutes using PL/I variants.
I'm going to suggest you didn't read my whole comment.
I sure did.
Burroughs B5500 was created in 1961 and executed far slower and had less memory than a PDP-11, yet it was able to have a full OS written in ESPOL, an ALGOL dialect with intrisics for CPU instructions and explicit UNSAFE statements.
So if a 10 year old machine by the time C was created for computers with " extra overhead was unbearable." and " what you do when you have a 1Mhz computer" was already able to execute an Algol dialect with enough performance, what does that say of C in regards to language design?
Burroughs B5500 was created in 1961 and executed far slower and had less memory than a PDP-11, yet it was able to have a full OS written in ESPOL, an ALGOL dialect with intrisics for CPU instructions and explicit UNSAFE statements.
So if a 10 year old machine by the time C was created for computers with " extra overhead was unbearable." and " what you do when you have a 1Mhz computer" was already able to execute an Algol dialect with enough performance, what does that say of C in regards to language design?
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?