Skip to content

Commit dfb80ee

Browse files
committed
Fix query.to_readonly().get_component_mut() soundness bug (#6401)
# Objective Fix the soundness issue outlined in #5866. In short the problem is that `query.to_readonly().get_component_mut::<T>()` can provide unsound mutable access to the component. This PR is an alternative to just removing the offending api. Given that `to_readonly` is a useful tool, I think this approach is a preferable short term solution. Long term I think theres a better solution out there, but we can find that on its own time. ## Solution Add what amounts to a "dirty flag" that marks Queries that have been converted to their read-only variant via `to_readonly` as dirty. When this flag is set to true, `get_component_mut` will fail with an error, preventing the unsound access.
1 parent dd7ff88 commit dfb80ee

File tree

2 files changed

+37
-11
lines changed

2 files changed

+37
-11
lines changed

crates/bevy_ecs/src/system/mod.rs

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -116,8 +116,8 @@ mod tests {
116116
query::{Added, Changed, Or, With, Without},
117117
schedule::{Schedule, Stage, SystemStage},
118118
system::{
119-
Commands, IntoSystem, Local, NonSend, NonSendMut, ParamSet, Query, RemovedComponents,
120-
Res, ResMut, Resource, System, SystemState,
119+
Commands, IntoSystem, Local, NonSend, NonSendMut, ParamSet, Query, QueryComponentError,
120+
RemovedComponents, Res, ResMut, Resource, System, SystemState,
121121
},
122122
world::{FromWorld, World},
123123
};
@@ -141,7 +141,7 @@ mod tests {
141141
#[derive(Component, Resource)]
142142
struct F;
143143

144-
#[derive(Component)]
144+
#[derive(Component, Debug)]
145145
struct W<T>(T);
146146

147147
#[derive(StageLabel)]
@@ -1163,4 +1163,17 @@ mod tests {
11631163
}
11641164
});
11651165
}
1166+
1167+
#[test]
1168+
fn readonly_query_get_mut_component_fails() {
1169+
let mut world = World::new();
1170+
let entity = world.spawn(W(42u32)).id();
1171+
run_system(&mut world, move |q: Query<&mut W<u32>>| {
1172+
let mut rq = q.to_readonly();
1173+
assert_eq!(
1174+
QueryComponentError::MissingWriteAccess,
1175+
rq.get_component_mut::<W<u32>>(entity).unwrap_err(),
1176+
);
1177+
});
1178+
}
11661179
}

crates/bevy_ecs/src/system/query.rs

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,12 @@ pub struct Query<'world, 'state, Q: WorldQuery, F: ReadOnlyWorldQuery = ()> {
278278
pub(crate) state: &'state QueryState<Q, F>,
279279
pub(crate) last_change_tick: u32,
280280
pub(crate) change_tick: u32,
281+
// SAFETY: This is used to ensure that `get_component_mut::<C>` properly fails when a Query writes C
282+
// and gets converted to a read-only query using `to_readonly`. Without checking this, `get_component_mut` relies on
283+
// QueryState's archetype_component_access, which will continue allowing write access to C after being cast to
284+
// the read-only variant. This whole situation is confusing and error prone. Ideally this is a temporary hack
285+
// until we sort out a cleaner alternative.
286+
pub(crate) force_read_only_component_access: bool,
281287
}
282288

283289
impl<'w, 's, Q: WorldQuery, F: ReadOnlyWorldQuery> std::fmt::Debug for Query<'w, 's, Q, F> {
@@ -301,6 +307,7 @@ impl<'w, 's, Q: WorldQuery, F: ReadOnlyWorldQuery> Query<'w, 's, Q, F> {
301307
change_tick: u32,
302308
) -> Self {
303309
Self {
310+
force_read_only_component_access: false,
304311
world,
305312
state,
306313
last_change_tick,
@@ -316,13 +323,14 @@ impl<'w, 's, Q: WorldQuery, F: ReadOnlyWorldQuery> Query<'w, 's, Q, F> {
316323
pub fn to_readonly(&self) -> Query<'_, 's, Q::ReadOnly, F::ReadOnly> {
317324
let new_state = self.state.as_readonly();
318325
// SAFETY: This is memory safe because it turns the query immutable.
319-
unsafe {
320-
Query::new(
321-
self.world,
322-
new_state,
323-
self.last_change_tick,
324-
self.change_tick,
325-
)
326+
Query {
327+
// SAFETY: this must be set to true or `get_component_mut` will be unsound. See the comments
328+
// on this field for more details
329+
force_read_only_component_access: true,
330+
world: self.world,
331+
state: new_state,
332+
last_change_tick: self.last_change_tick,
333+
change_tick: self.change_tick,
326334
}
327335
}
328336

@@ -1161,6 +1169,11 @@ impl<'w, 's, Q: WorldQuery, F: ReadOnlyWorldQuery> Query<'w, 's, Q, F> {
11611169
&self,
11621170
entity: Entity,
11631171
) -> Result<Mut<'_, T>, QueryComponentError> {
1172+
// SAFETY: this check is required to ensure soundness in the case of `to_readonly().get_component_mut()`
1173+
// See the comments on the `force_read_only_component_access` field for more info.
1174+
if self.force_read_only_component_access {
1175+
return Err(QueryComponentError::MissingWriteAccess);
1176+
}
11641177
let world = self.world;
11651178
let entity_ref = world
11661179
.get_entity(entity)
@@ -1411,7 +1424,7 @@ impl<'w, 's, Q: WorldQuery, F: ReadOnlyWorldQuery> IntoIterator for &'w mut Quer
14111424
}
14121425

14131426
/// An error that occurs when retrieving a specific [`Entity`]'s component from a [`Query`]
1414-
#[derive(Debug)]
1427+
#[derive(Debug, PartialEq, Eq)]
14151428
pub enum QueryComponentError {
14161429
MissingReadAccess,
14171430
MissingWriteAccess,

0 commit comments

Comments
 (0)