-
Notifications
You must be signed in to change notification settings - Fork 34
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
A soundness issue #41
Comments
Cells have been the bane of my existence :P
If I understand, this causes UB by creating a self-referencing struct that
is not labeled as self-referencing. Rust thinks `'a` must be some external
lifetime when really it's referring to itself. Problem being that the
lifetime of the `bar` argument needs to be `'this` so that we can use it to
build a self-referencing object, but then that allows setting its internal
reference to itself.
I'll take a crack at reducing this to a smaller example.
…On Tue, Oct 5, 2021 at 1:51 PM Frank Steffahn ***@***.***> wrote:
use std::cell::RefCell;
use ouroboros::self_referencing;
struct Bar<'a>(RefCell<(Option<&'a Bar<'a>>, String)>);
#[self_referencing]
struct Foo {
owner: (),
#[borrows(owner)]
#[not_covariant]
bar: Bar<'this>,
#[borrows(bar)]
#[not_covariant]
baz: &'this Bar<'this>,
}
impl Drop for Bar<'_> {
fn drop(&mut self) {
let r1 = self.0.get_mut();
let string_ref_1 = &mut r1.1;
let mut r2 = r1.0.unwrap().0.borrow_mut();
let string_ref_2 = &mut r2.1;
let s = &string_ref_1[..];
string_ref_2.clear();
string_ref_2.shrink_to_fit();
println!("{}", s); // prints garbage :-), use-after free
}
}
fn main() {
Foo::new(
(),
|_| Bar(RefCell::new((None, "Hello World!".to_owned()))),
|bar| {
bar.0.borrow_mut().0 = Some(bar);
bar
},
);
}
Don’t expect a deeper explanation from me before tomorrow 😃
—
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub
<#41>, or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AOL2YRJXMGXK3UJO3MD3QPLUFNQL7ANCNFSM5FMTTSPQ>
.
Triage notifications on the go with GitHub Mobile for iOS
<https://apps.apple.com/app/apple-store/id1477376905?ct=notification-email&mt=8&pt=524675>
or Android
<https://play.google.com/store/apps/details?id=com.github.android&referrer=utm_campaign%3Dnotification-email%26utm_medium%3Demail%26utm_source%3Dgithub>.
|
The best analogy I can come up with is that the problem is related what ordinarily in Rust is handled by the drop check. The problem is that Some demonstration use std::ptr;
struct CovariantNoDrop<'a>(*const &'a ());
struct CovariantWithDrop<'a>(*const &'a ());
struct InvariantNoDrop<'a>(*mut &'a ());
struct InvariantWithDrop<'a>(*mut &'a ());
impl Drop for CovariantWithDrop<'_> {
fn drop(&mut self) {}
}
impl Drop for InvariantWithDrop<'_> {
fn drop(&mut self) {}
}
fn test1() {
fn expects_same_lifetime<'a>(_: &'a CovariantNoDrop<'a>) {}
let x = CovariantNoDrop(ptr::null());
expects_same_lifetime(&x); // works fine
}
fn test2() {
fn expects_same_lifetime<'a>(_: &'a CovariantWithDrop<'a>) {}
let x = CovariantWithDrop(ptr::null());
expects_same_lifetime(&x); // works fine
}
fn test3() {
fn expects_same_lifetime<'a>(_: &'a InvariantNoDrop<'a>) {}
let x = InvariantNoDrop(ptr::null_mut());
expects_same_lifetime(&x); // works fine
}
fn test4() {
fn expects_same_lifetime<'a>(_: &'a InvariantWithDrop<'a>) {}
let x = InvariantWithDrop(ptr::null_mut());
expects_same_lifetime(&x); // error: x does not live long enough!
} Note that for mutable borrows, covariance doesn’t help either use std::ptr;
struct CovariantNoDrop<'a>(*const &'a ());
struct CovariantWithDrop<'a>(*const &'a ());
struct InvariantNoDrop<'a>(*mut &'a ());
struct InvariantWithDrop<'a>(*mut &'a ());
impl Drop for CovariantWithDrop<'_> {
fn drop(&mut self) {}
}
impl Drop for InvariantWithDrop<'_> {
fn drop(&mut self) {}
}
fn test1() {
fn expects_same_lifetime<'a>(_: &'a mut CovariantNoDrop<'a>) {}
let mut x = CovariantNoDrop(ptr::null());
expects_same_lifetime(&mut x); // works fine
}
fn test2() {
fn expects_same_lifetime<'a>(_: &'a mut CovariantWithDrop<'a>) {}
let mut x = CovariantWithDrop(ptr::null());
expects_same_lifetime(&mut x); // error: x does not live long enough!
}
fn test3() {
fn expects_same_lifetime<'a>(_: &'a mut InvariantNoDrop<'a>) {}
let mut x = InvariantNoDrop(ptr::null_mut());
expects_same_lifetime(&mut x); // works fine
}
fn test4() {
fn expects_same_lifetime<'a>(_: &'a mut InvariantWithDrop<'a>) {}
let mut x = InvariantWithDrop(ptr::null_mut());
expects_same_lifetime(&mut x); // error: x does not live long enough!
} |
Could this be leveraged by inserting some hidden code like this: fn hypothetical_bar<'a>() -> Bar<'a> { unimplemented!() }
fn check_bar_lifetime<'a>(_: &'a Bar<'a>) { }
fn checks() {
check_bar_lifetime(&hypothetical_bar());
} |
One potential fix I can think of would be: Rule out immutably borrowing non-covariant types that implement fn test(f: for<'this> fn(&'this ()) -> InvariantWithDrop<'this>) {
fn expect_same_lifetime<'this>(_: &'this InvariantWithDrop<'this>) {}
let foo = f(&());
expect_same_lifetime(&foo);
} whenever a borrowing field is borrowed by another field as in
and a similar test involving A different fix would be to look for some way to have multiple different #[self_referencing]
struct Foo {
owner: (),
#[borrows(owner)]
#[not_covariant]
bar: Bar<'this_owner>,
#[borrows(bar)]
#[not_covariant]
baz: &'this_bar Bar<'this_owner>,
} (syntax for declaring the struct TBD) The constructors as well as the It also seems reasonable to give the user the choice of whether they want a single homogeneous |
Yes, I think so. We kind-of posted at the same time :-) Of course this is to some degree a breaking change. Note that this interoperates nicely with types such as |
Thanks for the input, this is really helpful. I'll take some time to develop my understanding of the problem. I think the hidden code approach would be a good first step, then possibly add granular lifetimes later to allow getting around some of the restrictions. |
I don’t know what your level of understanding w.r.t. dropck is – quick explanation – the basic idea is that when you have a Rust’s drop check still allows this when W.r.t. covariance, why you can create a I haven’t 100% thought through the question of whether allowing immutable borrows like this for covariant types is okay in the context of |
Maybe the best approach is to just let borrowck evaluate the whole situation itself, by writing – for a struct E.g. for pub struct DocumentationExample {
int_data: i32,
float_data: f32,
#[borrows(int_data)]
int_reference: &'this i32,
#[borrows(mut float_data)]
float_reference: &'this mut f32,
} something like fn test_dropck(
int_data: i32,
float_data: f32,
int_reference_builder: impl for<'this> FnOnce(&'this i32) -> &'this i32,
float_reference_builder: impl for<'this> FnOnce(&'this mut f32) -> &'this mut f32
) {
let int_data = int_data;
let mut float_data = float_data;
let int_reference = int_reference_builder(&int_data);
let float_reference = float_reference_builder(&mut float_data);
BorrowedFields {
int_data: &int_data,
int_reference: &int_reference,
float_reference: &float_reference,
};
} |
Well, so now you generate something like fn check_if_okay_according_to_borrow_checker(
owner: (),
bar_builder: impl for<'this> ::core::ops::FnOnce(&'this ()) -> Bar<'this>,
baz_builder: impl for<'this> ::core::ops::FnOnce(&'this Bar<'this>) -> &'this Bar<'this>,
) {
let mut owner = ::ouroboros::macro_help::aliasable_boxed(owner);
let bar = bar_builder(&owner);
let mut bar = ::ouroboros::macro_help::aliasable_boxed(bar);
let baz = baz_builder(&bar);
BorrowedFields {
owner: &owner,
bar: &bar,
baz: &baz,
};
} The usage of Because of the aliasable boxes being introduced, now the struct struct Bar<'a>(RefCell<(Option<&'a Bar<'a>>, String)>);
#[self_referencing]
struct Foo {
owner: (),
#[borrows(owner)]
#[not_covariant]
bar: Bar<'this>,
#[borrows(bar)]
#[not_covariant]
baz: &'this Bar<'this>,
} is rejected even when It’s perhaps a separate question whether the approach that |
Here’s another example of a struct that doesn’t work in #[self_referencing]
struct Foo {
o: (),
#[borrows(mut o)]
r: &'this mut (),
#[borrows(mut r)]
#[not_covariant]
rr: &'this mut &'this mut (),
} (Another issue with this struct is that it’s misinferring the variance of |
I’m doing some code review of the latest commits here now 😅 😉 Why did you add the (redundant) second set of |
</codereview> Couldn’t find any other problems in those two commits so far. Well, except that the naming of the whole thing only talks about borrow checking, while the test (mostly) targets drop checking (dropck is AFAIK not really “part of” borrow checking). But I don’t really care about the naming of the |
Oh, now I get the |
@steffahn I've pushed some updates. lmk what you think:
|
I'll take a look tomorrow. |
These kinds of annotations seem wrong; the type
I think this isn’t necessary. The rules for a
If these rules are implemented, then there should never be a need for variance annotations on mutable references. Interestingly, the same can not be said about shared references. Only
is true for them, but we have
I’ll have to take a deeper look at the logic around covariance detection to figure out how best to change it. I think the main change that would need to happen is that the detection should feature a three-valued logic, returning whether
instead of the current two-valued logic, returning whether the type is know to be covariant or unknown. Usage of #[self_referencing]
struct Foo {
owner: u8,
#[borrows(owner)]
dependent: &'this u8
} you can own a Note that I’m not 100% certain about these things, but I personally would prefer the conservative approach to keep the Note – if that wasn't clear – that in my comment above
I’m exclusively referring to the usage of
Reviewing code like this is a bit suboptimal. If you want to follow my suggestion not to use Alternatively, I can also try to create a PR myself, if you want :-) |
Don’t expect a deeper explanation from me before tomorrow 😃
The text was updated successfully, but these errors were encountered: