Skip to content

Commit aaf384a

Browse files
committed
Panic on dropping NonSend in non-origin thread. (#6534)
# Objective Fixes #3310. Fixes #6282. Fixes #6278. Fixes #3666. ## Solution Split out `!Send` resources into `NonSendResources`. Add a `origin_thread_id` to all `!Send` Resources, check it on dropping `NonSendResourceData`, if there's a mismatch, panic. Moved all of the checks that `MainThreadValidator` would do into `NonSendResources` instead. All `!Send` resources now individually track which thread they were inserted from. This is validated against for every access, mutation, and drop that could be done against the value. A regression test using an altered version of the example from #3310 has been added. This is a stopgap solution for the current status quo. A full solution may involve fully removing `!Send` resources/components from `World`, which will likely require a much more thorough design on how to handle the existing in-engine and ecosystem use cases. This PR also introduces another breaking change: ```rust use bevy_ecs::prelude::*; #[derive(Resource)] struct Resource(u32); fn main() { let mut world = World::new(); world.insert_resource(Resource(1)); world.insert_non_send_resource(Resource(2)); let res = world.get_resource_mut::<Resource>().unwrap(); assert_eq!(res.0, 2); } ``` This code will run correctly on 0.9.1 but not with this PR, since NonSend resources and normal resources have become actual distinct concepts storage wise. ## Changelog Changed: Fix soundness bug with `World: Send`. Dropping a `World` that contains a `!Send` resource on the wrong thread will now panic. ## Migration Guide Normal resources and `NonSend` resources no longer share the same backing storage. If `R: Resource`, then `NonSend<R>` and `Res<R>` will return different instances from each other. If you are using both `Res<T>` and `NonSend<T>` (or their mutable variants), to fetch the same resources, it's strongly advised to use `Res<T>`.
1 parent 1b9c156 commit aaf384a

File tree

8 files changed

+369
-184
lines changed

8 files changed

+369
-184
lines changed

crates/bevy_ecs/src/lib.rs

+20-17
Original file line numberDiff line numberDiff line change
@@ -1267,6 +1267,15 @@ mod tests {
12671267
assert_eq!(*world.non_send_resource_mut::<i64>(), 456);
12681268
}
12691269

1270+
#[test]
1271+
fn non_send_resource_points_to_distinct_data() {
1272+
let mut world = World::default();
1273+
world.insert_resource(A(123));
1274+
world.insert_non_send_resource(A(456));
1275+
assert_eq!(*world.resource::<A>(), A(123));
1276+
assert_eq!(*world.non_send_resource::<A>(), A(456));
1277+
}
1278+
12701279
#[test]
12711280
#[should_panic]
12721281
fn non_send_resource_panic() {
@@ -1406,38 +1415,32 @@ mod tests {
14061415
assert_eq!(world.resource::<A>().0, 1);
14071416
}
14081417

1409-
#[test]
1410-
fn non_send_resource_scope() {
1411-
let mut world = World::default();
1412-
world.insert_non_send_resource(NonSendA::default());
1413-
world.resource_scope(|world: &mut World, mut value: Mut<NonSendA>| {
1414-
value.0 += 1;
1415-
assert!(!world.contains_resource::<NonSendA>());
1416-
});
1417-
assert_eq!(world.non_send_resource::<NonSendA>().0, 1);
1418-
}
1419-
14201418
#[test]
14211419
#[should_panic(
1422-
expected = "attempted to access NonSend resource bevy_ecs::tests::NonSendA off of the main thread"
1420+
expected = "Attempted to access or drop non-send resource bevy_ecs::tests::NonSendA from thread"
14231421
)]
1424-
fn non_send_resource_scope_from_different_thread() {
1422+
fn non_send_resource_drop_from_different_thread() {
14251423
let mut world = World::default();
14261424
world.insert_non_send_resource(NonSendA::default());
14271425

14281426
let thread = std::thread::spawn(move || {
1429-
// Accessing the non-send resource on a different thread
1427+
// Dropping the non-send resource on a different thread
14301428
// Should result in a panic
1431-
world.resource_scope(|_: &mut World, mut value: Mut<NonSendA>| {
1432-
value.0 += 1;
1433-
});
1429+
drop(world);
14341430
});
14351431

14361432
if let Err(err) = thread.join() {
14371433
std::panic::resume_unwind(err);
14381434
}
14391435
}
14401436

1437+
#[test]
1438+
fn non_send_resource_drop_from_same_thread() {
1439+
let mut world = World::default();
1440+
world.insert_non_send_resource(NonSendA::default());
1441+
drop(world);
1442+
}
1443+
14411444
#[test]
14421445
fn insert_overwrite_drop() {
14431446
let (dropck1, dropped1) = DropCk::new_pair();

crates/bevy_ecs/src/schedule/ambiguity_detection.rs

+4
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,7 @@ mod tests {
335335
fn read_only() {
336336
let mut world = World::new();
337337
world.insert_resource(R);
338+
world.insert_non_send_resource(R);
338339
world.spawn(A);
339340
world.init_resource::<Events<E>>();
340341

@@ -394,6 +395,7 @@ mod tests {
394395
fn nonsend() {
395396
let mut world = World::new();
396397
world.insert_resource(R);
398+
world.insert_non_send_resource(R);
397399

398400
let mut test_stage = SystemStage::parallel();
399401
test_stage
@@ -497,6 +499,7 @@ mod tests {
497499
fn ignore_all_ambiguities() {
498500
let mut world = World::new();
499501
world.insert_resource(R);
502+
world.insert_non_send_resource(R);
500503

501504
let mut test_stage = SystemStage::parallel();
502505
test_stage
@@ -513,6 +516,7 @@ mod tests {
513516
fn ambiguous_with_label() {
514517
let mut world = World::new();
515518
world.insert_resource(R);
519+
world.insert_non_send_resource(R);
516520

517521
#[derive(SystemLabel)]
518522
struct IgnoreMe;

crates/bevy_ecs/src/storage/mod.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,6 @@ pub use table::*;
1414
pub struct Storages {
1515
pub sparse_sets: SparseSets,
1616
pub tables: Tables,
17-
pub resources: Resources,
17+
pub resources: Resources<true>,
18+
pub non_send_resources: Resources<false>,
1819
}

crates/bevy_ecs/src/storage/resource.rs

+111-41
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,61 @@ use crate::archetype::ArchetypeComponentId;
22
use crate::component::{ComponentId, ComponentTicks, Components, TickCells};
33
use crate::storage::{Column, SparseSet, TableRow};
44
use bevy_ptr::{OwningPtr, Ptr, UnsafeCellDeref};
5+
use std::{mem::ManuallyDrop, thread::ThreadId};
56

67
/// The type-erased backing storage and metadata for a single resource within a [`World`].
78
///
9+
/// If `SEND` is false, values of this type will panic if dropped from a different thread.
10+
///
811
/// [`World`]: crate::world::World
9-
pub struct ResourceData {
10-
column: Column,
12+
pub struct ResourceData<const SEND: bool> {
13+
column: ManuallyDrop<Column>,
14+
type_name: String,
1115
id: ArchetypeComponentId,
16+
origin_thread_id: Option<ThreadId>,
17+
}
18+
19+
impl<const SEND: bool> Drop for ResourceData<SEND> {
20+
fn drop(&mut self) {
21+
if self.is_present() {
22+
// If this thread is already panicking, panicking again will cause
23+
// the entire process to abort. In this case we choose to avoid
24+
// dropping or checking this altogether and just leak the column.
25+
if std::thread::panicking() {
26+
return;
27+
}
28+
self.validate_access();
29+
}
30+
// SAFETY: Drop is only called once upon dropping the ResourceData
31+
// and is inaccessible after this as the parent ResourceData has
32+
// been dropped. The validate_access call above will check that the
33+
// data is dropped on the thread it was inserted from.
34+
unsafe {
35+
ManuallyDrop::drop(&mut self.column);
36+
}
37+
}
1238
}
1339

14-
impl ResourceData {
40+
impl<const SEND: bool> ResourceData<SEND> {
1541
/// The only row in the underlying column.
1642
const ROW: TableRow = TableRow::new(0);
1743

44+
#[inline]
45+
fn validate_access(&self) {
46+
if SEND {
47+
return;
48+
}
49+
if self.origin_thread_id != Some(std::thread::current().id()) {
50+
// Panic in tests, as testing for aborting is nearly impossible
51+
panic!(
52+
"Attempted to access or drop non-send resource {} from thread {:?} on a thread {:?}. This is not allowed. Aborting.",
53+
self.type_name,
54+
self.origin_thread_id,
55+
std::thread::current().id()
56+
);
57+
}
58+
}
59+
1860
/// Returns true if the resource is populated.
1961
#[inline]
2062
pub fn is_present(&self) -> bool {
@@ -28,9 +70,16 @@ impl ResourceData {
2870
}
2971

3072
/// Gets a read-only pointer to the underlying resource, if available.
73+
///
74+
/// # Panics
75+
/// If `SEND` is false, this will panic if a value is present and is not accessed from the
76+
/// original thread it was inserted from.
3177
#[inline]
3278
pub fn get_data(&self) -> Option<Ptr<'_>> {
33-
self.column.get_data(Self::ROW)
79+
self.column.get_data(Self::ROW).map(|res| {
80+
self.validate_access();
81+
res
82+
})
3483
}
3584

3685
/// Gets a read-only reference to the change ticks of the underlying resource, if available.
@@ -39,83 +88,98 @@ impl ResourceData {
3988
self.column.get_ticks(Self::ROW)
4089
}
4190

91+
/// # Panics
92+
/// If `SEND` is false, this will panic if a value is present and is not accessed from the
93+
/// original thread it was inserted in.
4294
#[inline]
4395
pub(crate) fn get_with_ticks(&self) -> Option<(Ptr<'_>, TickCells<'_>)> {
44-
self.column.get(Self::ROW)
96+
self.column.get(Self::ROW).map(|res| {
97+
self.validate_access();
98+
res
99+
})
45100
}
46101

47102
/// Inserts a value into the resource. If a value is already present
48103
/// it will be replaced.
49104
///
50-
/// # Safety
51-
/// `value` must be valid for the underlying type for the resource.
52-
///
53-
/// The underlying type must be [`Send`] or be inserted from the main thread.
54-
/// This can be validated with [`World::validate_non_send_access_untyped`].
105+
/// # Panics
106+
/// If `SEND` is false, this will panic if a value is present and is not replaced from
107+
/// the original thread it was inserted in.
55108
///
56-
/// [`World::validate_non_send_access_untyped`]: crate::world::World::validate_non_send_access_untyped
109+
/// # Safety
110+
/// - `value` must be valid for the underlying type for the resource.
57111
#[inline]
58112
pub(crate) unsafe fn insert(&mut self, value: OwningPtr<'_>, change_tick: u32) {
59113
if self.is_present() {
114+
self.validate_access();
60115
self.column.replace(Self::ROW, value, change_tick);
61116
} else {
117+
if !SEND {
118+
self.origin_thread_id = Some(std::thread::current().id());
119+
}
62120
self.column.push(value, ComponentTicks::new(change_tick));
63121
}
64122
}
65123

66124
/// Inserts a value into the resource with a pre-existing change tick. If a
67125
/// value is already present it will be replaced.
68126
///
69-
/// # Safety
70-
/// `value` must be valid for the underlying type for the resource.
71-
///
72-
/// The underlying type must be [`Send`] or be inserted from the main thread.
73-
/// This can be validated with [`World::validate_non_send_access_untyped`].
127+
/// # Panics
128+
/// If `SEND` is false, this will panic if a value is present and is not replaced from
129+
/// the original thread it was inserted in.
74130
///
75-
/// [`World::validate_non_send_access_untyped`]: crate::world::World::validate_non_send_access_untyped
131+
/// # Safety
132+
/// - `value` must be valid for the underlying type for the resource.
76133
#[inline]
77134
pub(crate) unsafe fn insert_with_ticks(
78135
&mut self,
79136
value: OwningPtr<'_>,
80137
change_ticks: ComponentTicks,
81138
) {
82139
if self.is_present() {
140+
self.validate_access();
83141
self.column.replace_untracked(Self::ROW, value);
84142
*self.column.get_added_ticks_unchecked(Self::ROW).deref_mut() = change_ticks.added;
85143
*self
86144
.column
87145
.get_changed_ticks_unchecked(Self::ROW)
88146
.deref_mut() = change_ticks.changed;
89147
} else {
148+
if !SEND {
149+
self.origin_thread_id = Some(std::thread::current().id());
150+
}
90151
self.column.push(value, change_ticks);
91152
}
92153
}
93154

94155
/// Removes a value from the resource, if present.
95156
///
96-
/// # Safety
97-
/// The underlying type must be [`Send`] or be removed from the main thread.
98-
/// This can be validated with [`World::validate_non_send_access_untyped`].
99-
///
100-
/// The removed value must be used or dropped.
101-
///
102-
/// [`World::validate_non_send_access_untyped`]: crate::world::World::validate_non_send_access_untyped
157+
/// # Panics
158+
/// If `SEND` is false, this will panic if a value is present and is not removed from the
159+
/// original thread it was inserted from.
103160
#[inline]
104161
#[must_use = "The returned pointer to the removed component should be used or dropped"]
105-
pub(crate) unsafe fn remove(&mut self) -> Option<(OwningPtr<'_>, ComponentTicks)> {
106-
self.column.swap_remove_and_forget(Self::ROW)
162+
pub(crate) fn remove(&mut self) -> Option<(OwningPtr<'_>, ComponentTicks)> {
163+
if SEND {
164+
self.column.swap_remove_and_forget(Self::ROW)
165+
} else {
166+
self.is_present()
167+
.then(|| self.validate_access())
168+
.and_then(|_| self.column.swap_remove_and_forget(Self::ROW))
169+
}
107170
}
108171

109172
/// Removes a value from the resource, if present, and drops it.
110173
///
111-
/// # Safety
112-
/// The underlying type must be [`Send`] or be removed from the main thread.
113-
/// This can be validated with [`World::validate_non_send_access_untyped`].
114-
///
115-
/// [`World::validate_non_send_access_untyped`]: crate::world::World::validate_non_send_access_untyped
174+
/// # Panics
175+
/// If `SEND` is false, this will panic if a value is present and is not
176+
/// accessed from the original thread it was inserted in.
116177
#[inline]
117-
pub(crate) unsafe fn remove_and_drop(&mut self) {
118-
self.column.clear();
178+
pub(crate) fn remove_and_drop(&mut self) {
179+
if self.is_present() {
180+
self.validate_access();
181+
self.column.clear();
182+
}
119183
}
120184
}
121185

@@ -124,11 +188,11 @@ impl ResourceData {
124188
/// [`Resource`]: crate::system::Resource
125189
/// [`World`]: crate::world::World
126190
#[derive(Default)]
127-
pub struct Resources {
128-
resources: SparseSet<ComponentId, ResourceData>,
191+
pub struct Resources<const SEND: bool> {
192+
resources: SparseSet<ComponentId, ResourceData<SEND>>,
129193
}
130194

131-
impl Resources {
195+
impl<const SEND: bool> Resources<SEND> {
132196
/// The total number of resources stored in the [`World`]
133197
///
134198
/// [`World`]: crate::world::World
@@ -138,7 +202,7 @@ impl Resources {
138202
}
139203

140204
/// Iterate over all resources that have been initialized, i.e. given a [`ComponentId`]
141-
pub fn iter(&self) -> impl Iterator<Item = (ComponentId, &ResourceData)> {
205+
pub fn iter(&self) -> impl Iterator<Item = (ComponentId, &ResourceData<SEND>)> {
142206
self.resources.iter().map(|(id, data)| (*id, data))
143207
}
144208

@@ -153,31 +217,37 @@ impl Resources {
153217

154218
/// Gets read-only access to a resource, if it exists.
155219
#[inline]
156-
pub fn get(&self, component_id: ComponentId) -> Option<&ResourceData> {
220+
pub fn get(&self, component_id: ComponentId) -> Option<&ResourceData<SEND>> {
157221
self.resources.get(component_id)
158222
}
159223

160224
/// Gets mutable access to a resource, if it exists.
161225
#[inline]
162-
pub(crate) fn get_mut(&mut self, component_id: ComponentId) -> Option<&mut ResourceData> {
226+
pub(crate) fn get_mut(&mut self, component_id: ComponentId) -> Option<&mut ResourceData<SEND>> {
163227
self.resources.get_mut(component_id)
164228
}
165229

166230
/// Fetches or initializes a new resource and returns back it's underlying column.
167231
///
168232
/// # Panics
169233
/// Will panic if `component_id` is not valid for the provided `components`
234+
/// If `SEND` is false, this will panic if `component_id`'s `ComponentInfo` is not registered as being `Send` + `Sync`.
170235
pub(crate) fn initialize_with(
171236
&mut self,
172237
component_id: ComponentId,
173238
components: &Components,
174239
f: impl FnOnce() -> ArchetypeComponentId,
175-
) -> &mut ResourceData {
240+
) -> &mut ResourceData<SEND> {
176241
self.resources.get_or_insert_with(component_id, || {
177242
let component_info = components.get_info(component_id).unwrap();
243+
if SEND {
244+
assert!(component_info.is_send_and_sync());
245+
}
178246
ResourceData {
179-
column: Column::with_capacity(component_info, 1),
247+
column: ManuallyDrop::new(Column::with_capacity(component_info, 1)),
248+
type_name: String::from(component_info.name()),
180249
id: f(),
250+
origin_thread_id: None,
181251
}
182252
})
183253
}

0 commit comments

Comments
 (0)