Skip to content

Commit a9d2a9e

Browse files
authored
Make QueryFilter an unsafe trait (#14790)
# Objective It's possible to create UB using an implementation of `QueryFilter` that performs mutable access, but that does not violate any documented safety invariants. This code: ```rust #[derive(Component)] struct Foo(usize); // This derive is a simple way to get a valid WorldQuery impl. The QueryData impl isn't used. #[derive(QueryData)] #[query_data(mutable)] struct BadFilter<'w> { foo: &'w mut Foo, } impl QueryFilter for BadFilter<'_> { const IS_ARCHETYPAL: bool = false; unsafe fn filter_fetch( fetch: &mut Self::Fetch<'_>, entity: Entity, table_row: TableRow, ) -> bool { // SAFETY: fetch and filter_fetch have the same safety requirements let f: &mut usize = &mut unsafe { Self::fetch(fetch, entity, table_row) }.foo.0; println!("Got &mut at {f:p}"); true } } let mut world = World::new(); world.spawn(Foo(0)); world.run_system_once(|query: Query<&Foo, BadFilter>| { let f: &usize = &query.iter().next().unwrap().0; println!("Got & at {f:p}"); query.iter().next().unwrap(); println!("Still have & at {f:p}"); }); ``` prints: ``` Got &mut at 0x1924b92dfb0 Got & at 0x1924b92dfb0 Got &mut at 0x1924b92dfb0 Still have & at 0x1924b92dfb0 ``` Which means it had an `&` and `&mut` alive at the same time. The only `unsafe` there is around `Self::fetch`, but I believe that call correctly upholds the safety invariant, and matches what `Added` and `Changed` do. ## Solution Make `QueryFilter` an unsafe trait and document the requirement that the `WorldQuery` implementation be read-only. ## Migration Guide `QueryFilter` is now an `unsafe trait`. If you were manually implementing it, you will need to verify that the `WorldQuery` implementation is read-only and then add the `unsafe` keyword to the `impl`.
1 parent 4b78ba0 commit a9d2a9e

File tree

2 files changed

+20
-8
lines changed

2 files changed

+20
-8
lines changed

crates/bevy_ecs/macros/src/query_filter.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,8 @@ pub fn derive_query_filter_impl(input: TokenStream) -> TokenStream {
120120
);
121121

122122
let filter_impl = quote! {
123-
impl #user_impl_generics #path::query::QueryFilter
123+
// SAFETY: This only performs access that subqueries perform, and they impl `QueryFilter` and so perform no mutable access.
124+
unsafe impl #user_impl_generics #path::query::QueryFilter
124125
for #struct_name #user_ty_generics #user_where_clauses {
125126
const IS_ARCHETYPAL: bool = true #(&& <#field_types>::IS_ARCHETYPAL)*;
126127

crates/bevy_ecs/src/query/filter.rs

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -70,12 +70,17 @@ use std::{cell::UnsafeCell, marker::PhantomData};
7070
/// [`matches_component_set`]: Self::matches_component_set
7171
/// [`Query`]: crate::system::Query
7272
/// [`State`]: Self::State
73+
///
74+
/// # Safety
75+
///
76+
/// The [`WorldQuery`] implementation must not take any mutable access.
77+
/// This is the same safety requirement as [`ReadOnlyQueryData`](crate::query::ReadOnlyQueryData).
7378
#[diagnostic::on_unimplemented(
7479
message = "`{Self}` is not a valid `Query` filter",
7580
label = "invalid `Query` filter",
7681
note = "a `QueryFilter` typically uses a combination of `With<T>` and `Without<T>` statements"
7782
)]
78-
pub trait QueryFilter: WorldQuery {
83+
pub unsafe trait QueryFilter: WorldQuery {
7984
/// Returns true if (and only if) this Filter relies strictly on archetypes to limit which
8085
/// components are accessed by the Query.
8186
///
@@ -201,7 +206,8 @@ unsafe impl<T: Component> WorldQuery for With<T> {
201206
}
202207
}
203208

204-
impl<T: Component> QueryFilter for With<T> {
209+
// SAFETY: WorldQuery impl performs no access at all
210+
unsafe impl<T: Component> QueryFilter for With<T> {
205211
const IS_ARCHETYPAL: bool = true;
206212

207213
#[inline(always)]
@@ -311,7 +317,8 @@ unsafe impl<T: Component> WorldQuery for Without<T> {
311317
}
312318
}
313319

314-
impl<T: Component> QueryFilter for Without<T> {
320+
// SAFETY: WorldQuery impl performs no access at all
321+
unsafe impl<T: Component> QueryFilter for Without<T> {
315322
const IS_ARCHETYPAL: bool = true;
316323

317324
#[inline(always)]
@@ -490,7 +497,8 @@ macro_rules! impl_or_query_filter {
490497
}
491498
}
492499

493-
impl<$($filter: QueryFilter),*> QueryFilter for Or<($($filter,)*)> {
500+
// SAFETY: This only performs access that subqueries perform, and they impl `QueryFilter` and so perform no mutable access.
501+
unsafe impl<$($filter: QueryFilter),*> QueryFilter for Or<($($filter,)*)> {
494502
const IS_ARCHETYPAL: bool = true $(&& $filter::IS_ARCHETYPAL)*;
495503

496504
#[inline(always)]
@@ -512,7 +520,8 @@ macro_rules! impl_tuple_query_filter {
512520
#[allow(non_snake_case)]
513521
#[allow(clippy::unused_unit)]
514522
$(#[$meta])*
515-
impl<$($name: QueryFilter),*> QueryFilter for ($($name,)*) {
523+
// SAFETY: This only performs access that subqueries perform, and they impl `QueryFilter` and so perform no mutable access.
524+
unsafe impl<$($name: QueryFilter),*> QueryFilter for ($($name,)*) {
516525
const IS_ARCHETYPAL: bool = true $(&& $name::IS_ARCHETYPAL)*;
517526

518527
#[inline(always)]
@@ -734,7 +743,8 @@ unsafe impl<T: Component> WorldQuery for Added<T> {
734743
}
735744
}
736745

737-
impl<T: Component> QueryFilter for Added<T> {
746+
// SAFETY: WorldQuery impl performs only read access on ticks
747+
unsafe impl<T: Component> QueryFilter for Added<T> {
738748
const IS_ARCHETYPAL: bool = false;
739749
#[inline(always)]
740750
unsafe fn filter_fetch(
@@ -949,7 +959,8 @@ unsafe impl<T: Component> WorldQuery for Changed<T> {
949959
}
950960
}
951961

952-
impl<T: Component> QueryFilter for Changed<T> {
962+
// SAFETY: WorldQuery impl performs only read access on ticks
963+
unsafe impl<T: Component> QueryFilter for Changed<T> {
953964
const IS_ARCHETYPAL: bool = false;
954965

955966
#[inline(always)]

0 commit comments

Comments
 (0)