-
Notifications
You must be signed in to change notification settings - Fork 3
New issue
Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? # to your account
Ref::map_val allows pointer leak #5
Comments
Can you quickly explain what should fail in your opinion, why and how? |
I don't know what should fail exactly. Somehow If it's not clear what's wrong with current behavior, there's another example:
This prints:
It is "use after free": |
Or even more clear example:
This code results in |
escape_debug() is roughly the Rust equivalent of python's repr on str. Fixes d-e-s-o#5.
Thanks for the examples! I hope to be able to look at this problem more closely by the end of the week. |
It appears that the lifetimes are messed up for pub fn map_val<U: Sized, F>(orig: Ref<'b, T>, f: F) -> RefVal<'b, U>
where F: FnOnce(&'b T) -> U
// vs.
pub fn map<U: ?Sized, F>(orig: Ref<'b, T>, f: F) -> Ref<'b, U>
where F: FnOnce(&T) -> &U So our This example illustrates that things work as expected when the #[test]
fn should_fail_but_it_passes() {
use std::ops::Deref;
let y: &String = {
let s = RefCell::new("hallo".into());
let b = RefCell::borrow(&s);
let x: RefVal<&String> = Ref::map_val(b, |s| s);
x.deref()
};
assert_eq!(y.as_str(), "hallo");
}
|
Interestingly, #[test]
fn should_fail_but_it_passes() {
use std::ops::Deref;
let st = "hallo".to_string();
let s = RefCell::new(&st);
let y: &String = {
let b = RefCell::borrow(&s);
let x: Ref<&String> = Ref::map(b, |s| s);
x.deref()
};
// assert not borrowed
RefCell::borrow_mut(&s);
// but pointer to data is still accessible
assert_eq!(y.as_str(), "hallo");
} This example just works. But should it? Which gets me back to the question why we think it is a problem. I also don't think that this is really a use-after-free. As I established in the previous example, if the variable being referenced is dropped the code won't compile. It's only the borrow that is "freed". |
removed |
No wait. |
There's nothing wrong with RefCell.
So when calling |
Hm yeah, I noticed that too after experimenting a little more. Sigh |
I stopped understand anything:
This code compiles. But should it? This line:
|
Moreover, both these lines are correct:
|
OK, seems like Rust does auto-dereferencing. Since when?
|
OK, it is known. users |
OK, new minimal test case to reproduce the problem:
|
Hm, but isn't that the same problem I hit? |
(I am inferring that based on the function signature: |
Yes, you are right. Thinking... |
What I find very interesting is that when I make --- src/cell.rs
+++ src/cell.rs
@@ -579,19 +579,19 @@ impl<'b, T: ?Sized> Ref<'b, T> {
}
}
- /// Make a new `RefVal` from the borrowed data.
- ///
- /// The `RefCell` is already immutably borrowed, so this operation
- /// cannot fail.
- #[inline]
- pub fn map_val<U: Sized, F>(orig: Ref<'b, T>, f: F) -> RefVal<'b, U>
- where F: FnOnce(&'b T) -> U
- {
- RefVal {
- value: f(orig.value),
- borrow: orig.borrow,
- }
- }
+ ///// Make a new `RefVal` from the borrowed data.
+ /////
+ ///// The `RefCell` is already immutably borrowed, so this operation
+ ///// cannot fail.
+ //#[inline]
+ //pub fn map_val<U: Sized, F>(orig: Ref<'b, T>, f: F) -> RefVal<'b, U>
+ // where F: FnOnce(&'b T) -> U
+ //{
+ // RefVal {
+ // value: f(orig.value),
+ // borrow: orig.borrow,
+ // }
+ //}
}
impl<T: ?Sized + fmt::Display> fmt::Display for Ref<'_, T> {
@@ -641,10 +641,10 @@ impl<'b, T: ?Sized> RefMut<'b, T> {
/// The `RefCell` is already immutably borrowed, so this operation
/// cannot fail.
#[inline]
- pub fn map_val<U: Sized, F>(orig: RefMut<'b, T>, f: F) -> RefValMut<'b, U>
+ pub fn map_val<U: Sized, F>(orig: RefMut<'b, T>, f: F) -> RefVal<'b, U>
where F: FnOnce(&'b mut T) -> U
{
- RefValMut {
+ RefVal {
value: f(orig.value),
borrow: orig.borrow,
}
@@ -733,7 +733,7 @@ impl<T: ?Sized + fmt::Display> fmt::Display for RefMut<'_, T> {
/// See the [module-level documentation](index.html) for more.
pub struct RefVal<'b, T> {
value: T,
- borrow: BorrowRef<'b>,
+ borrow: BorrowRefMut<'b>,
}
impl<'b, T> RefVal<'b, T> { #[test]
fn should_fail_but_it_passes() {
use std::ops::Deref;
use std::ops::DerefMut;
let s = RefCell::new("hallo".into());
let y: &String = {
let b = RefCell::borrow_mut(&s);
let x: RefVal<&mut String> = RefMut::map_val(b, |s| s);
x.deref()
};
let mut z = RefCell::borrow_mut(&s);
*z = "hihi".to_string();
assert_eq!(y.as_str(), "hallo");
}
But then I checked the definitions of both structs and couldn't really find something that explains that difference to me. |
(I came to that because for |
Yeah, it definitely has something to do with that. This example correctly fails to compile (everything is #[test]
fn should_fail_but_it_passes() {
use std::ops::Deref;
use std::ops::DerefMut;
let s = RefCell::new("hallo".into());
let y: &String = {
let b = RefCell::borrow_mut(&s);
let x: RefValMut<&mut String> = RefMut::map_val(b, |s| s);
x.deref()
};
let mut z = RefCell::borrow_mut(&s);
*z = "hihi".to_string();
assert_eq!(y.as_str(), "hallo");
} Now let's make the following change: --- tests/cell.rs
+++ tests/cell.rs
@@ -481,7 +481,7 @@ fn should_fail_but_it_passes() {
let s = RefCell::new("hallo".into());
let y: &String = {
let b = RefCell::borrow_mut(&s);
- let x: RefValMut<&mut String> = RefMut::map_val(b, |s| s);
+ let x: RefValMut<&String> = RefMut::map_val(b, |s| s as &String);
x.deref()
};
And now it compiles. |
So if we change in first example everything from mutable to immutables, it starts to compile. Looks suspicious. |
So, the minimal example would be: Compiles fine with
But fails to compile with
|
So looks like there's special handling of |
I guess it's something like that. I am not sure your minimal example actually illustrates that very case, though. But then I think my brain is fried. Will see if I have a clear thought tomorrow. |
The problem, how I understand it (again). Basically impl<T> Ref<'_, T> {
fn deref<'r, 's>(&'s self) -> &'r T where 's: 'r { ... }
} Which means that lifetime of the returned pointer ( Translating this example to impl RefVal<'_, T> {
fn deref<'s>(&'s self) -> &'s T where 's: T { ... }
} Which means that that lifetime of But this is not valid Rust syntax. |
Yep. We are somehow missing the connection from the anonymous RefVal lifetime to that of What I would do next (but haven't found the time to) is to create a truly minimal example showing the same unsafety on nothing more than a combination of a struct, a map, and a deref (or whatever we need) -- decoupled from the existing |
I tried to reproduce it with safe code. I'm certain it is impossible. Because it's not possible to implement Rust is safe. Just not flexible enough. |
Not sure I am following. What are you suggesting is happening here? I am not using unsafe Rust and yet we have an memory unsafety issue, don't we (otherwise how can we segfault)? So that leaves three possibilities from my point of view:
I agree that 1) is unlikely.
Okay, but internal mutability is part of the language (by language I am including the standard library). |
Yes, I think this is it. |
BTW, until Rust is changed to support this use case, this sad and incomplete workaround is possible:
|
"Minimal" test harness: cell-issue-5.tar.gz |
FYI, I've posed this "challenge" to a wider audience in the Rust user forum and there were a few responses, most notably https://users.rust-lang.org/t/unsafety-with-advanced-version-of-refcell/29323/4 I haven't yet fully gone through it. |
Hi folks, I spent a few hours today trying to make this work, as it seemed like it might be useful for a project of mine. I figured I might as well report my findings. The reason why the current implementation is unsafe is the choice of lifetime in The signature is currently:
We want to replace This is exactly how
We can try to apply the same principle to
Unfortunately, this (correctly!) prevents I think it will only be possible to make this work the way you've envisaged it once we have higher-kinded type parameters. Something like this:
Unfortunately this kind of syntax is far away on the horizon. Even GATs wouldn't be enough to make it work; we'd need full-fledged higher-kinded type parameters. Sorry for the bad news! If somebody wants to prove me wrong then I'm all ears. |
Many thanks for the analysis! I'll see if it makes sense to point out this unsafety in the README or some place visible. |
Don't know yet how to fix it.
The text was updated successfully, but these errors were encountered: