Skip to content

Ability to UseAfterFree in master branch #1032

@AngelicosPhosphoros

Description

@AngelicosPhosphoros

Bevy version
From Cargo.lock

[[package]]
name = "bevy"
version = "0.3.0"
source = "git+https://github.com/bevyengine/bevy.git#3d386a77b406ee65d6965ad91b29ea3acb8bcf3a"
dependencies = [
 "bevy_internal",
]

Operating system & version

Windows 10 but I guess it is not relevant

What you did

This sample program. Notice 'static lifetime of Res (ResMut or Query works too, I believe).

use std::sync::{Mutex, MutexGuard};

use bevy::prelude::*;
use once_cell::sync::OnceCell;

static INSTANCE: OnceCell<Mutex<Option<Res<'static, String>>>> = OnceCell::new();

fn main() {
   INSTANCE.get_or_init(||Mutex::new(None));
   App::build()
       .add_resource("HelloWorld".to_string())
       .add_system(ub_system.system())
       .run();
   let guard: MutexGuard<Option<Res<'static, String>>> = INSTANCE.get().expect("uninited?").lock().expect("Failed to acquire lock");
   let resource: &Res<'static, String> = guard.as_ref().unwrap();
   println!("Wow, I read freed memory with such data: {:?}", (**resource).as_bytes());
}

fn ub_system(string: Res<'static, String>){
   let mut guard = INSTANCE.get().expect("uninited?").lock().expect("Failed to acquire lock");
   *guard = Some(string);
}

What you expected to happen

This code should not compile because this is UB.

What actually happened

In my case it prints some garbage.

Wow, I read freed memory with such data: [0, 223, 3, 195, 196, 1, 0, 0, 176, 169]
PS D:\UserFolder\poc_bevy_unsound> cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.31s
     Running `target\debug\poc_bevy_unsound.exe`
Wow, I read freed memory with such data: [224, 84, 1, 115, 128, 1, 0, 0, 32, 151]
PS D:\UserFolder\poc_bevy_unsound> cargo run --release
    Finished release [optimized] target(s) in 0.29s
     Running `target\release\poc_bevy_unsound.exe`
Wow, I read freed memory with such data: [112, 58, 118, 247, 229, 1, 0, 0, 112, 132]
PS D:\UserFolder\poc_bevy_unsound> cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.28s
     Running `target\debug\poc_bevy_unsound.exe`
Wow, I read freed memory with such data: [144, 58, 46, 167, 239, 1, 0, 0, 48, 97]

Additional information

I recently tried to implement own ECS system for my game and bevy was my inspiration. I implement it mostly for fun and I found same bug in my ECS during implementation.
We discussed it on Reddit and I found a solution.
Look at this playground link.

So I suggest:

  1. Add lifetime parameter to the IntoSystem trait Let assume it named as 'app.
  2. Use this lifetime in implementation of system for functions and get_param method. Add 'app constraints to requested types for function.
  3. Make system method in IntoSystem trait unsafe (because it is).
  4. Change add_system method of App to accept IntoSystem trait implementation and use it like
fn my_system(v: Res<T>){}
App::build().add_system(my_system)

And implement it like:

fn add_system<'a, T>(&'a mut self, sys: T) where T: 'static + IntoSystem<'a>{
    let system = unsafe{ sys.system() };
    // old code goes here
}

This would prevent passing function that accepts Res<'static, T> into App unless it is 'static itself but mutation of 'static variable available only in unsafe code so it is fine.

Note: Code example above doesn't compile on Bevy 0.3.0 from crates.io.

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-ECSEntities, components, systems, and eventsC-BugAn unexpected or incorrect behavior

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions