Skip to content

Reduce runtime panics through SystemParam validation #15276

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
merged 33 commits into from
Sep 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
74911e5
dummy validate_param
MiniaczQ Sep 17, 2024
f64cb53
partial ci fix
MiniaczQ Sep 17, 2024
bf1aec3
revert some doc changes
MiniaczQ Sep 17, 2024
65678a5
system validate param
MiniaczQ Sep 17, 2024
7c080ec
simplest param validation
MiniaczQ Sep 18, 2024
5d06655
validate param in executors
MiniaczQ Sep 18, 2024
0fc4e0c
remove safety section
MiniaczQ Sep 18, 2024
01e0a11
Merge branch 'main' into res-acq
MiniaczQ Sep 18, 2024
1d40de1
simple exec
MiniaczQ Sep 18, 2024
0b158e0
ci fix
MiniaczQ Sep 18, 2024
5658b23
test invalid param skipping
MiniaczQ Sep 18, 2024
92eaf1a
correct docs
MiniaczQ Sep 18, 2024
b20c7af
add non-access methods for resources existence
MiniaczQ Sep 18, 2024
74210d4
lots
MiniaczQ Sep 18, 2024
03ca7ad
ci fixes
MiniaczQ Sep 18, 2024
a950089
Merge branch 'main' into res-acq
MiniaczQ Sep 18, 2024
b0ed278
Merge branch 'main' into res-acq
MiniaczQ Sep 19, 2024
99a8e18
address comments
MiniaczQ Sep 21, 2024
1efe8cd
lints
MiniaczQ Sep 21, 2024
a1ef231
fix impl
MiniaczQ Sep 21, 2024
c9519d4
Merge branch 'main' into res-acq
MiniaczQ Sep 21, 2024
376f55c
todo for extract pram
MiniaczQ Sep 21, 2024
6e4ca5a
better docs for validate
MiniaczQ Sep 22, 2024
84b31d9
fix single threaded executors
MiniaczQ Sep 22, 2024
ccae63b
default validate impl
MiniaczQ Sep 22, 2024
a83ddaa
Merge branch 'main' into res-acq
MiniaczQ Sep 22, 2024
7e0d0ab
impl extract system param
MiniaczQ Sep 22, 2024
dd5d1d7
lifetime and is some and
MiniaczQ Sep 22, 2024
a3ecf8e
non mut world and better docs
MiniaczQ Sep 22, 2024
127ea9e
system update arch
MiniaczQ Sep 22, 2024
9950375
safety and param set
MiniaczQ Sep 22, 2024
3b4a47b
fix lint
MiniaczQ Sep 22, 2024
dbf174b
Merge branch 'main' into res-acq
MiniaczQ Sep 22, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions crates/bevy_ecs/macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,15 @@ pub fn impl_param_set(_input: TokenStream) -> TokenStream {
<(#(#param,)*) as SystemParam>::apply(state, system_meta, world);
}

#[inline]
unsafe fn validate_param<'w, 's>(
state: &'s Self::State,
system_meta: &SystemMeta,
world: UnsafeWorldCell<'w>,
) -> bool {
<(#(#param,)*) as SystemParam>::validate_param(state, system_meta, world)
}

#[inline]
unsafe fn get_param<'w, 's>(
state: &'s mut Self::State,
Expand Down Expand Up @@ -512,6 +521,16 @@ pub fn derive_system_param(input: TokenStream) -> TokenStream {
<#fields_alias::<'_, '_, #punctuated_generic_idents> as #path::system::SystemParam>::queue(&mut state.state, system_meta, world);
}

#[inline]
unsafe fn validate_param<'w, 's>(
state: &'s Self::State,
system_meta: &#path::system::SystemMeta,
world: #path::world::unsafe_world_cell::UnsafeWorldCell<'w>,
) -> bool {
<(#(#tuple_types,)*) as #path::system::SystemParam>::validate_param(&state.state, system_meta, world)
}

#[inline]
unsafe fn get_param<'w, 's>(
state: &'s mut Self::State,
system_meta: &#path::system::SystemMeta,
Expand Down
8 changes: 4 additions & 4 deletions crates/bevy_ecs/src/schedule/condition.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ pub trait Condition<Marker, In = ()>: sealed::Condition<Marker, In> {
///
/// # Examples
///
/// ```should_panic
/// ```
/// use bevy_ecs::prelude::*;
///
/// #[derive(Resource, PartialEq)]
Expand All @@ -89,7 +89,7 @@ pub trait Condition<Marker, In = ()>: sealed::Condition<Marker, In> {
/// # let mut world = World::new();
/// # fn my_system() {}
/// app.add_systems(
/// // The `resource_equals` run condition will panic since we don't initialize `R`,
/// // The `resource_equals` run condition will fail since we don't initialize `R`,
/// // just like if we used `Res<R>` in a system.
/// my_system.run_if(resource_equals(R(0))),
/// );
Expand Down Expand Up @@ -130,7 +130,7 @@ pub trait Condition<Marker, In = ()>: sealed::Condition<Marker, In> {
///
/// # Examples
///
/// ```should_panic
/// ```
/// use bevy_ecs::prelude::*;
///
/// #[derive(Resource, PartialEq)]
Expand All @@ -140,7 +140,7 @@ pub trait Condition<Marker, In = ()>: sealed::Condition<Marker, In> {
/// # let mut world = World::new();
/// # fn my_system() {}
/// app.add_systems(
/// // The `resource_equals` run condition will panic since we don't initialize `R`,
/// // The `resource_equals` run condition will fail since we don't initialize `R`,
/// // just like if we used `Res<R>` in a system.
/// my_system.run_if(resource_equals(R(0))),
/// );
Expand Down
97 changes: 97 additions & 0 deletions crates/bevy_ecs/src/schedule/executor/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,3 +176,100 @@ mod __rust_begin_short_backtrace {
black_box(system.run((), world))
}
}

#[macro_export]
/// Emits a warning about system being skipped.
macro_rules! warn_system_skipped {
($ty:literal, $sys:expr) => {
bevy_utils::tracing::warn!(
"{} {} was skipped due to inaccessible system parameters.",
$ty,
$sys
)
};
}

#[cfg(test)]
mod tests {
use crate::{
self as bevy_ecs,
prelude::{IntoSystemConfigs, IntoSystemSetConfigs, Resource, Schedule, SystemSet},
schedule::ExecutorKind,
system::{Commands, In, IntoSystem, Res},
world::World,
};

#[derive(Resource)]
struct R1;

#[derive(Resource)]
struct R2;

const EXECUTORS: [ExecutorKind; 3] = [
ExecutorKind::Simple,
ExecutorKind::SingleThreaded,
ExecutorKind::MultiThreaded,
];

#[test]
fn invalid_system_param_skips() {
for executor in EXECUTORS {
invalid_system_param_skips_core(executor);
}
}

fn invalid_system_param_skips_core(executor: ExecutorKind) {
let mut world = World::new();
let mut schedule = Schedule::default();
schedule.set_executor_kind(executor);
schedule.add_systems(
(
// Combined systems get skipped together.
(|mut commands: Commands| {
commands.insert_resource(R1);
})
.pipe(|_: In<()>, _: Res<R1>| {}),
// This system depends on a system that is always skipped.
|mut commands: Commands| {
commands.insert_resource(R2);
},
)
.chain(),
);
schedule.run(&mut world);
assert!(world.get_resource::<R1>().is_none());
assert!(world.get_resource::<R2>().is_some());
}

#[derive(SystemSet, Hash, Debug, PartialEq, Eq, Clone)]
struct S1;

#[test]
fn invalid_condition_param_skips_system() {
for executor in EXECUTORS {
invalid_condition_param_skips_system_core(executor);
}
}

fn invalid_condition_param_skips_system_core(executor: ExecutorKind) {
let mut world = World::new();
let mut schedule = Schedule::default();
schedule.set_executor_kind(executor);
schedule.configure_sets(S1.run_if(|_: Res<R1>| true));
schedule.add_systems((
// System gets skipped if system set run conditions fail validation.
(|mut commands: Commands| {
commands.insert_resource(R1);
})
.in_set(S1),
// System gets skipped if run conditions fail validation.
(|mut commands: Commands| {
commands.insert_resource(R2);
})
.run_if(|_: Res<R2>| true),
));
schedule.run(&mut world);
assert!(world.get_resource::<R1>().is_none());
assert!(world.get_resource::<R2>().is_none());
}
}
33 changes: 29 additions & 4 deletions crates/bevy_ecs/src/schedule/executor/multi_threaded.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ use crate::{
query::Access,
schedule::{is_apply_deferred, BoxedCondition, ExecutorKind, SystemExecutor, SystemSchedule},
system::BoxedSystem,
warn_system_skipped,
world::{unsafe_world_cell::UnsafeWorldCell, World},
};

Expand Down Expand Up @@ -519,15 +520,16 @@ impl ExecutorState {
/// the system's conditions: this includes conditions for the system
/// itself, and conditions for any of the system's sets.
/// * `update_archetype_component` must have been called with `world`
/// for each run condition in `conditions`.
/// for the system as well as system and system set's run conditions.
unsafe fn should_run(
&mut self,
system_index: usize,
_system: &BoxedSystem,
system: &BoxedSystem,
conditions: &mut Conditions,
world: UnsafeWorldCell,
) -> bool {
let mut should_run = !self.skipped_systems.contains(system_index);

for set_idx in conditions.sets_with_conditions_of_systems[system_index].ones() {
if self.evaluated_sets.contains(set_idx) {
continue;
Expand Down Expand Up @@ -566,6 +568,19 @@ impl ExecutorState {

should_run &= system_conditions_met;

// SAFETY:
// - The caller ensures that `world` has permission to read any data
// required by the system.
// - `update_archetype_component_access` has been called for system.
let valid_params = unsafe { system.validate_param_unsafe(world) };

if !valid_params {
warn_system_skipped!("System", system.name());
self.skipped_systems.insert(system_index);
}

should_run &= valid_params;

should_run
}

Expand Down Expand Up @@ -731,8 +746,18 @@ unsafe fn evaluate_and_fold_conditions(
conditions
.iter_mut()
.map(|condition| {
// SAFETY: The caller ensures that `world` has permission to
// access any data required by the condition.
// SAFETY:
// - The caller ensures that `world` has permission to read any data
// required by the condition.
// - `update_archetype_component_access` has been called for condition.
if !unsafe { condition.validate_param_unsafe(world) } {
warn_system_skipped!("Condition", condition.name());
return false;
}
// SAFETY:
// - The caller ensures that `world` has permission to read any data
// required by the condition.
// - `update_archetype_component_access` has been called for condition.
unsafe { __rust_begin_short_backtrace::readonly_run_unsafe(&mut **condition, world) }
})
.fold(true, |acc, res| acc && res)
Expand Down
19 changes: 17 additions & 2 deletions crates/bevy_ecs/src/schedule/executor/simple.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use crate::{
schedule::{
executor::is_apply_deferred, BoxedCondition, ExecutorKind, SystemExecutor, SystemSchedule,
},
warn_system_skipped,
world::World,
};

Expand Down Expand Up @@ -79,6 +80,15 @@ impl SystemExecutor for SimpleExecutor {

should_run &= system_conditions_met;

let system = &mut schedule.systems[system_index];
let valid_params = system.validate_param(world);

if !valid_params {
warn_system_skipped!("System", system.name());
}

should_run &= valid_params;

#[cfg(feature = "trace")]
should_run_span.exit();

Expand All @@ -89,7 +99,6 @@ impl SystemExecutor for SimpleExecutor {
continue;
}

let system = &mut schedule.systems[system_index];
if is_apply_deferred(system) {
continue;
}
Expand Down Expand Up @@ -128,7 +137,13 @@ fn evaluate_and_fold_conditions(conditions: &mut [BoxedCondition], world: &mut W
#[allow(clippy::unnecessary_fold)]
conditions
.iter_mut()
.map(|condition| __rust_begin_short_backtrace::readonly_run(&mut **condition, world))
.map(|condition| {
if !condition.validate_param(world) {
warn_system_skipped!("Condition", condition.name());
return false;
}
__rust_begin_short_backtrace::readonly_run(&mut **condition, world)
})
.fold(true, |acc, res| acc && res)
}

Expand Down
19 changes: 17 additions & 2 deletions crates/bevy_ecs/src/schedule/executor/single_threaded.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use std::panic::AssertUnwindSafe;

use crate::{
schedule::{is_apply_deferred, BoxedCondition, ExecutorKind, SystemExecutor, SystemSchedule},
warn_system_skipped,
world::World,
};

Expand Down Expand Up @@ -85,6 +86,15 @@ impl SystemExecutor for SingleThreadedExecutor {

should_run &= system_conditions_met;

let system = &mut schedule.systems[system_index];
let valid_params = system.validate_param(world);

if !valid_params {
warn_system_skipped!("System", system.name());
}

should_run &= valid_params;

#[cfg(feature = "trace")]
should_run_span.exit();

Expand All @@ -95,7 +105,6 @@ impl SystemExecutor for SingleThreadedExecutor {
continue;
}

let system = &mut schedule.systems[system_index];
if is_apply_deferred(system) {
self.apply_deferred(schedule, world);
continue;
Expand Down Expand Up @@ -160,6 +169,12 @@ fn evaluate_and_fold_conditions(conditions: &mut [BoxedCondition], world: &mut W
#[allow(clippy::unnecessary_fold)]
conditions
.iter_mut()
.map(|condition| __rust_begin_short_backtrace::readonly_run(&mut **condition, world))
.map(|condition| {
if !condition.validate_param(world) {
warn_system_skipped!("Condition", condition.name());
return false;
}
__rust_begin_short_backtrace::readonly_run(&mut **condition, world)
})
.fold(true, |acc, res| acc && res)
}
5 changes: 5 additions & 0 deletions crates/bevy_ecs/src/system/adapter_system.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,11 @@ where
self.system.queue_deferred(world);
}

#[inline]
unsafe fn validate_param_unsafe(&self, world: UnsafeWorldCell) -> bool {
self.system.validate_param_unsafe(world)
}

fn initialize(&mut self, world: &mut crate::prelude::World) {
self.system.initialize(world);
}
Expand Down
6 changes: 6 additions & 0 deletions crates/bevy_ecs/src/system/combinator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ where
)
}

#[inline]
fn apply_deferred(&mut self, world: &mut World) {
self.a.apply_deferred(world);
self.b.apply_deferred(world);
Expand All @@ -204,6 +205,11 @@ where
self.b.queue_deferred(world);
}

#[inline]
unsafe fn validate_param_unsafe(&self, world: UnsafeWorldCell) -> bool {
self.a.validate_param_unsafe(world) && self.b.validate_param_unsafe(world)
}

fn initialize(&mut self, world: &mut World) {
self.a.initialize(world);
self.b.initialize(world);
Expand Down
Loading