Skip to content
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

[Merged by Bors] - yeet unsound lifetime annotations on Query methods #4243

Closed
wants to merge 3 commits into from

Conversation

BoxyUwU
Copy link
Member

@BoxyUwU BoxyUwU commented Mar 17, 2022

Objective

Continuation of #2964 (I really should have checked other methods when I made that PR)

yeet unsound lifetime annotations on Query methods.
Example unsoundness:

use bevy::prelude::*;

fn main() {
    App::new().add_startup_system(bar).add_system(foo).run();
}

pub fn bar(mut cmds: Commands) {
    let e = cmds.spawn().insert(Foo { a: 10 }).id();
    cmds.insert_resource(e);
}

#[derive(Component, Debug, PartialEq, Eq)]
pub struct Foo {
    a: u32,
}
pub fn foo(mut query: Query<&mut Foo>, e: Res<Entity>) {
    dbg!("hi");
    {
        let data: &Foo = query.get(*e).unwrap();
        let data2: Mut<Foo> = query.get_mut(*e).unwrap();
        assert_eq!(data, &*data2); // oops UB
    }

    {
        let data: &Foo = query.single();
        let data2: Mut<Foo> = query.single_mut();
        assert_eq!(data, &*data2); // oops UB
    }

    {
        let data: &Foo = query.get_single().unwrap();
        let data2: Mut<Foo> = query.get_single_mut().unwrap();
        assert_eq!(data, &*data2); // oops UB
    }

    {
        let data: &Foo = query.iter().next().unwrap();
        let data2: Mut<Foo> = query.iter_mut().next().unwrap();
        assert_eq!(data, &*data2); // oops UB
    }

    {
        let mut opt_data: Option<&Foo> = None;
        let mut opt_data_2: Option<Mut<Foo>> = None;
        query.for_each(|data| opt_data = Some(data));
        query.for_each_mut(|data| opt_data_2 = Some(data));
        assert_eq!(opt_data.unwrap(), &*opt_data_2.unwrap()); // oops UB
    }
    dbg!("bye");
}

Solution

yeet unsound lifetime annotations on Query methods

@BoxyUwU BoxyUwU added the A-ECS Entities, components, systems, and events label Mar 17, 2022
@github-actions github-actions bot added the S-Needs-Triage This issue needs to be labelled label Mar 17, 2022
@BoxyUwU BoxyUwU added C-Bug An unexpected or incorrect behavior P-High This is particularly urgent, and deserves immediate attention and removed S-Needs-Triage This issue needs to be labelled labels Mar 17, 2022
@Guvante
Copy link
Contributor

Guvante commented Mar 17, 2022

Safety comments would help for the new unsafe calls otherwise LGTM

@BoxyUwU
Copy link
Member Author

BoxyUwU commented Mar 17, 2022

I do not know if the new unsafe code is sound or not 😃 I do not understand rendering code whatsoever, the code is exactly as sound as it was before this PR though all that's change is that we're more explicit about "oops this code might cause UB"

Copy link
Contributor

@Guvante Guvante left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

git blame to explain the problem with the PR comment is as good as any other solution

@Guvante
Copy link
Contributor

Guvante commented Mar 19, 2022

I arbitrarily chose 'w for the read only fetch I added... If we swapped it and the mutable ones we can satisfy the new tests. <WriteFetch<T> as Fetch<'w, 's>>::Item == Mut<'s, T> and <ReadOnlyWriteFetch<T> as Fetch<'w, 's>>::Item == & 's T

@cart cart added this to the Bevy 0.7 milestone Mar 19, 2022
@cart
Copy link
Member

cart commented Mar 19, 2022

This is definitely a welcome / needed fix, but forcing user-facing render code to use unsafe is "suboptimal". @BoxyUwU and I discussed a lot of solutions, but the one I'm most in favor of is adding "inner" method variants for "read only queries". This is safe, although @BoxyUwU rightly pointed out that allowing 'w lifetimes in read only queries might limit future apis (such as WorldCell-like apis). They also pointed out that allowing 'w does increase the burden on Query developers / Bevy ECS developers to maintain the proper invariants.

That being said, I still think the inner approach is the best interim solution (for the 0.7 release), as it doesn't require any changes to how Queries work (or how render code works). It is a "surgical" fix. Just a minor breaking change to add the _inner prefix where relevant in renderer code.

A lot of other ideas were thrown around, such Query wrappers, QueryMut variants, using QueryState instead of Queries on the render side, and others. But those are all much bigger / risker changes that deserve more time and thought.

Consider the latest commit "a proposal" though. We can revert it if anyone wants to push back.

@Guvante
Copy link
Contributor

Guvante commented Mar 19, 2022

I was able to get the new tests to pass by making this change Guvante@0372566 to have a more concrete reference.

@cart
Copy link
Member

cart commented Mar 19, 2022

I was able to get the new tests to pass by making this change Guvante@0372566 to have a more concrete reference.

That is "wrong" from my perspective. It is tying the components to the internal QueryState lifetime instead of the internal World lifetime. The components do not belong to QueryState, they belong to World. If we're going to do that, we should just give up on split lifetimes and go back to a "merged" lifetime.

@cart
Copy link
Member

cart commented Mar 19, 2022

And I personally think we shouldn't give up on split lifetimes, because the renderer relies on getting components with world lifetimes.

@Guvante
Copy link
Contributor

Guvante commented Mar 19, 2022

I assumed we didn't actually want to lift a mutable lifetime to 'w and that change only impacts mutable lifetimes. (I mean technically ReadOnlyWriteFetch is immutable but that is more how you can transform an &mut into &)

@Guvante
Copy link
Contributor

Guvante commented Mar 19, 2022

Oh I accidentally revised out my other point. I assumed &self was the wanted lifetime for mutable things and it looked like most of the call sites were &'s self.

Copy link
Contributor

@Guvante Guvante left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assumed that &'s self would shorten the lifetime of it similar to '_ but it seems that isn't always the case so a broader solution is better.

@cart
Copy link
Member

cart commented Mar 22, 2022

bors r+

bors bot pushed a commit that referenced this pull request Mar 22, 2022
# Objective
Continuation of #2964 (I really should have checked other methods when I made that PR)

yeet unsound lifetime annotations on `Query` methods.
Example unsoundness:
```rust
use bevy::prelude::*;

fn main() {
    App::new().add_startup_system(bar).add_system(foo).run();
}

pub fn bar(mut cmds: Commands) {
    let e = cmds.spawn().insert(Foo { a: 10 }).id();
    cmds.insert_resource(e);
}

#[derive(Component, Debug, PartialEq, Eq)]
pub struct Foo {
    a: u32,
}
pub fn foo(mut query: Query<&mut Foo>, e: Res<Entity>) {
    dbg!("hi");
    {
        let data: &Foo = query.get(*e).unwrap();
        let data2: Mut<Foo> = query.get_mut(*e).unwrap();
        assert_eq!(data, &*data2); // oops UB
    }

    {
        let data: &Foo = query.single();
        let data2: Mut<Foo> = query.single_mut();
        assert_eq!(data, &*data2); // oops UB
    }

    {
        let data: &Foo = query.get_single().unwrap();
        let data2: Mut<Foo> = query.get_single_mut().unwrap();
        assert_eq!(data, &*data2); // oops UB
    }

    {
        let data: &Foo = query.iter().next().unwrap();
        let data2: Mut<Foo> = query.iter_mut().next().unwrap();
        assert_eq!(data, &*data2); // oops UB
    }

    {
        let mut opt_data: Option<&Foo> = None;
        let mut opt_data_2: Option<Mut<Foo>> = None;
        query.for_each(|data| opt_data = Some(data));
        query.for_each_mut(|data| opt_data_2 = Some(data));
        assert_eq!(opt_data.unwrap(), &*opt_data_2.unwrap()); // oops UB
    }
    dbg!("bye");
}

```

## Solution
yeet unsound lifetime annotations on `Query` methods

Co-authored-by: Carter Anderson <mcanders1@gmail.com>
@bors bors bot changed the title yeet unsound lifetime annotations on Query methods [Merged by Bors] - yeet unsound lifetime annotations on Query methods Mar 22, 2022
@bors bors bot closed this Mar 22, 2022
aevyrie pushed a commit to aevyrie/bevy that referenced this pull request Jun 7, 2022
# Objective
Continuation of bevyengine#2964 (I really should have checked other methods when I made that PR)

yeet unsound lifetime annotations on `Query` methods.
Example unsoundness:
```rust
use bevy::prelude::*;

fn main() {
    App::new().add_startup_system(bar).add_system(foo).run();
}

pub fn bar(mut cmds: Commands) {
    let e = cmds.spawn().insert(Foo { a: 10 }).id();
    cmds.insert_resource(e);
}

#[derive(Component, Debug, PartialEq, Eq)]
pub struct Foo {
    a: u32,
}
pub fn foo(mut query: Query<&mut Foo>, e: Res<Entity>) {
    dbg!("hi");
    {
        let data: &Foo = query.get(*e).unwrap();
        let data2: Mut<Foo> = query.get_mut(*e).unwrap();
        assert_eq!(data, &*data2); // oops UB
    }

    {
        let data: &Foo = query.single();
        let data2: Mut<Foo> = query.single_mut();
        assert_eq!(data, &*data2); // oops UB
    }

    {
        let data: &Foo = query.get_single().unwrap();
        let data2: Mut<Foo> = query.get_single_mut().unwrap();
        assert_eq!(data, &*data2); // oops UB
    }

    {
        let data: &Foo = query.iter().next().unwrap();
        let data2: Mut<Foo> = query.iter_mut().next().unwrap();
        assert_eq!(data, &*data2); // oops UB
    }

    {
        let mut opt_data: Option<&Foo> = None;
        let mut opt_data_2: Option<Mut<Foo>> = None;
        query.for_each(|data| opt_data = Some(data));
        query.for_each_mut(|data| opt_data_2 = Some(data));
        assert_eq!(opt_data.unwrap(), &*opt_data_2.unwrap()); // oops UB
    }
    dbg!("bye");
}

```

## Solution
yeet unsound lifetime annotations on `Query` methods

Co-authored-by: Carter Anderson <mcanders1@gmail.com>
ItsDoot pushed a commit to ItsDoot/bevy that referenced this pull request Feb 1, 2023
# Objective
Continuation of bevyengine#2964 (I really should have checked other methods when I made that PR)

yeet unsound lifetime annotations on `Query` methods.
Example unsoundness:
```rust
use bevy::prelude::*;

fn main() {
    App::new().add_startup_system(bar).add_system(foo).run();
}

pub fn bar(mut cmds: Commands) {
    let e = cmds.spawn().insert(Foo { a: 10 }).id();
    cmds.insert_resource(e);
}

#[derive(Component, Debug, PartialEq, Eq)]
pub struct Foo {
    a: u32,
}
pub fn foo(mut query: Query<&mut Foo>, e: Res<Entity>) {
    dbg!("hi");
    {
        let data: &Foo = query.get(*e).unwrap();
        let data2: Mut<Foo> = query.get_mut(*e).unwrap();
        assert_eq!(data, &*data2); // oops UB
    }

    {
        let data: &Foo = query.single();
        let data2: Mut<Foo> = query.single_mut();
        assert_eq!(data, &*data2); // oops UB
    }

    {
        let data: &Foo = query.get_single().unwrap();
        let data2: Mut<Foo> = query.get_single_mut().unwrap();
        assert_eq!(data, &*data2); // oops UB
    }

    {
        let data: &Foo = query.iter().next().unwrap();
        let data2: Mut<Foo> = query.iter_mut().next().unwrap();
        assert_eq!(data, &*data2); // oops UB
    }

    {
        let mut opt_data: Option<&Foo> = None;
        let mut opt_data_2: Option<Mut<Foo>> = None;
        query.for_each(|data| opt_data = Some(data));
        query.for_each_mut(|data| opt_data_2 = Some(data));
        assert_eq!(opt_data.unwrap(), &*opt_data_2.unwrap()); // oops UB
    }
    dbg!("bye");
}

```

## Solution
yeet unsound lifetime annotations on `Query` methods

Co-authored-by: Carter Anderson <mcanders1@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-ECS Entities, components, systems, and events C-Bug An unexpected or incorrect behavior P-High This is particularly urgent, and deserves immediate attention
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants