Skip to content

Commit 78e763f

Browse files
BoxyUwUProfLander
authored andcommitted
Fix unsoundnes in insert remove and despawn (bevyengine#7805)
`EntityMut::move_entity_from_remove` had two soundness bugs: - When removing the entity from the archetype, the swapped entity had its table row updated to the same as the removed entity's - When removing the entity from the table, the swapped entity did not have its table row updated `BundleInsert::insert` had two/three soundness bugs - When moving an entity to a new archetype from an `insert`, the swapped entity had its table row set to a different entities - When moving an entity to a new table from an `insert`, the swapped entity did not have its table row updated See added tests for examples that trigger those bugs `EntityMut::despawn` had two soundness bugs - When despawning an entity, the swapped entity had its table row set to a different entities even if the table didnt change - When despawning an entity, the swapped entity did not have its table row updated
1 parent 46fc7d7 commit 78e763f

File tree

2 files changed

+230
-5
lines changed

2 files changed

+230
-5
lines changed

crates/bevy_ecs/src/bundle.rs

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -555,7 +555,16 @@ impl<'a, 'b> BundleInserter<'a, 'b> {
555555
InsertBundleResult::NewArchetypeSameTable { new_archetype } => {
556556
let result = self.archetype.swap_remove(location.archetype_row);
557557
if let Some(swapped_entity) = result.swapped_entity {
558-
self.entities.set(swapped_entity.index(), location);
558+
let swapped_location = self.entities.get(swapped_entity).unwrap();
559+
self.entities.set(
560+
swapped_entity.index(),
561+
EntityLocation {
562+
archetype_id: swapped_location.archetype_id,
563+
archetype_row: location.archetype_row,
564+
table_id: swapped_location.table_id,
565+
table_row: swapped_location.table_row,
566+
},
567+
);
559568
}
560569
let new_location = new_archetype.allocate(entity, result.table_row);
561570
self.entities.set(entity.index(), new_location);
@@ -583,7 +592,16 @@ impl<'a, 'b> BundleInserter<'a, 'b> {
583592
} => {
584593
let result = self.archetype.swap_remove(location.archetype_row);
585594
if let Some(swapped_entity) = result.swapped_entity {
586-
self.entities.set(swapped_entity.index(), location);
595+
let swapped_location = self.entities.get(swapped_entity).unwrap();
596+
self.entities.set(
597+
swapped_entity.index(),
598+
EntityLocation {
599+
archetype_id: swapped_location.archetype_id,
600+
archetype_row: location.archetype_row,
601+
table_id: swapped_location.table_id,
602+
table_row: swapped_location.table_row,
603+
},
604+
);
587605
}
588606
// PERF: store "non bundle" components in edge, then just move those to avoid
589607
// redundant copies
@@ -608,6 +626,15 @@ impl<'a, 'b> BundleInserter<'a, 'b> {
608626
.add(swapped_location.archetype_id.index())
609627
};
610628

629+
self.entities.set(
630+
swapped_entity.index(),
631+
EntityLocation {
632+
archetype_id: swapped_location.archetype_id,
633+
archetype_row: swapped_location.archetype_row,
634+
table_id: swapped_location.table_id,
635+
table_row: result.table_row,
636+
},
637+
);
611638
swapped_archetype
612639
.set_entity_table_row(swapped_location.archetype_row, result.table_row);
613640
}

crates/bevy_ecs/src/world/entity_ref.rs

Lines changed: 201 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -371,8 +371,19 @@ impl<'w> EntityMut<'w> {
371371
) {
372372
let old_archetype = &mut archetypes[old_archetype_id];
373373
let remove_result = old_archetype.swap_remove(old_location.archetype_row);
374+
// if an entity was moved into this entity's archetype row, update its archetype row
374375
if let Some(swapped_entity) = remove_result.swapped_entity {
375-
entities.set(swapped_entity.index(), old_location);
376+
let swapped_location = entities.get(swapped_entity).unwrap();
377+
378+
entities.set(
379+
swapped_entity.index(),
380+
EntityLocation {
381+
archetype_id: swapped_location.archetype_id,
382+
archetype_row: old_location.archetype_row,
383+
table_id: swapped_location.table_id,
384+
table_row: swapped_location.table_row,
385+
},
386+
);
376387
}
377388
let old_table_row = remove_result.table_row;
378389
let old_table_id = old_archetype.table_id();
@@ -395,9 +406,19 @@ impl<'w> EntityMut<'w> {
395406
// SAFETY: move_result.new_row is a valid position in new_archetype's table
396407
let new_location = new_archetype.allocate(entity, move_result.new_row);
397408

398-
// if an entity was moved into this entity's table spot, update its table row
409+
// if an entity was moved into this entity's table row, update its table row
399410
if let Some(swapped_entity) = move_result.swapped_entity {
400411
let swapped_location = entities.get(swapped_entity).unwrap();
412+
413+
entities.set(
414+
swapped_entity.index(),
415+
EntityLocation {
416+
archetype_id: swapped_location.archetype_id,
417+
archetype_row: swapped_location.archetype_row,
418+
table_id: swapped_location.table_id,
419+
table_row: old_location.table_row,
420+
},
421+
);
401422
archetypes[swapped_location.archetype_id]
402423
.set_entity_table_row(swapped_location.archetype_row, old_table_row);
403424
}
@@ -493,10 +514,19 @@ impl<'w> EntityMut<'w> {
493514
}
494515
let remove_result = archetype.swap_remove(location.archetype_row);
495516
if let Some(swapped_entity) = remove_result.swapped_entity {
517+
let swapped_location = world.entities.get(swapped_entity).unwrap();
496518
// SAFETY: swapped_entity is valid and the swapped entity's components are
497519
// moved to the new location immediately after.
498520
unsafe {
499-
world.entities.set(swapped_entity.index(), location);
521+
world.entities.set(
522+
swapped_entity.index(),
523+
EntityLocation {
524+
archetype_id: swapped_location.archetype_id,
525+
archetype_row: location.archetype_row,
526+
table_id: swapped_location.table_id,
527+
table_row: swapped_location.table_row,
528+
},
529+
);
500530
}
501531
}
502532
table_row = remove_result.table_row;
@@ -513,6 +543,19 @@ impl<'w> EntityMut<'w> {
513543

514544
if let Some(moved_entity) = moved_entity {
515545
let moved_location = world.entities.get(moved_entity).unwrap();
546+
// SAFETY: `moved_entity` is valid and the provided `EntityLocation` accurately reflects
547+
// the current location of the entity and its component data.
548+
unsafe {
549+
world.entities.set(
550+
moved_entity.index(),
551+
EntityLocation {
552+
archetype_id: moved_location.archetype_id,
553+
archetype_row: moved_location.archetype_row,
554+
table_id: moved_location.table_id,
555+
table_row,
556+
},
557+
);
558+
}
516559
world.archetypes[moved_location.archetype_id]
517560
.set_entity_table_row(moved_location.archetype_row, table_row);
518561
}
@@ -907,4 +950,159 @@ mod tests {
907950
// Ensure that the location has been properly updated.
908951
assert!(entity.location() != old_location);
909952
}
953+
954+
// regression test for https://github.com/bevyengine/bevy/pull/7805
955+
#[test]
956+
fn removing_sparse_updates_archetype_row() {
957+
#[derive(Component, PartialEq, Debug)]
958+
struct Dense(u8);
959+
960+
#[derive(Component)]
961+
#[component(storage = "SparseSet")]
962+
struct Sparse;
963+
964+
let mut world = World::new();
965+
let e1 = world.spawn((Dense(0), Sparse)).id();
966+
let e2 = world.spawn((Dense(1), Sparse)).id();
967+
968+
world.entity_mut(e1).remove::<Sparse>();
969+
assert_eq!(world.entity(e2).get::<Dense>().unwrap(), &Dense(1));
970+
}
971+
972+
// regression test for https://github.com/bevyengine/bevy/pull/7805
973+
#[test]
974+
fn removing_dense_updates_table_row() {
975+
#[derive(Component, PartialEq, Debug)]
976+
struct Dense(u8);
977+
978+
#[derive(Component)]
979+
#[component(storage = "SparseSet")]
980+
struct Sparse;
981+
982+
let mut world = World::new();
983+
let e1 = world.spawn((Dense(0), Sparse)).id();
984+
let e2 = world.spawn((Dense(1), Sparse)).id();
985+
986+
world.entity_mut(e1).remove::<Dense>();
987+
assert_eq!(world.entity(e2).get::<Dense>().unwrap(), &Dense(1));
988+
}
989+
990+
// regression test for https://github.com/bevyengine/bevy/pull/7805
991+
#[test]
992+
fn inserting_sparse_updates_archetype_row() {
993+
#[derive(Component, PartialEq, Debug)]
994+
struct Dense(u8);
995+
996+
#[derive(Component)]
997+
#[component(storage = "SparseSet")]
998+
struct Sparse;
999+
1000+
let mut world = World::new();
1001+
let e1 = world.spawn(Dense(0)).id();
1002+
let e2 = world.spawn(Dense(1)).id();
1003+
1004+
world.entity_mut(e1).insert(Sparse);
1005+
assert_eq!(world.entity(e2).get::<Dense>().unwrap(), &Dense(1));
1006+
}
1007+
1008+
// regression test for https://github.com/bevyengine/bevy/pull/7805
1009+
#[test]
1010+
fn inserting_dense_updates_archetype_row() {
1011+
#[derive(Component, PartialEq, Debug)]
1012+
struct Dense(u8);
1013+
1014+
#[derive(Component)]
1015+
struct Dense2;
1016+
1017+
#[derive(Component)]
1018+
#[component(storage = "SparseSet")]
1019+
struct Sparse;
1020+
1021+
let mut world = World::new();
1022+
let e1 = world.spawn(Dense(0)).id();
1023+
let e2 = world.spawn(Dense(1)).id();
1024+
1025+
world.entity_mut(e1).insert(Sparse).remove::<Sparse>();
1026+
1027+
// archetype with [e2, e1]
1028+
// table with [e1, e2]
1029+
1030+
world.entity_mut(e2).insert(Dense2);
1031+
1032+
assert_eq!(world.entity(e1).get::<Dense>().unwrap(), &Dense(0));
1033+
}
1034+
1035+
#[test]
1036+
fn inserting_dense_updates_table_row() {
1037+
#[derive(Component, PartialEq, Debug)]
1038+
struct Dense(u8);
1039+
1040+
#[derive(Component)]
1041+
struct Dense2;
1042+
1043+
#[derive(Component)]
1044+
#[component(storage = "SparseSet")]
1045+
struct Sparse;
1046+
1047+
let mut world = World::new();
1048+
let e1 = world.spawn(Dense(0)).id();
1049+
let e2 = world.spawn(Dense(1)).id();
1050+
1051+
world.entity_mut(e1).insert(Sparse).remove::<Sparse>();
1052+
1053+
// archetype with [e2, e1]
1054+
// table with [e1, e2]
1055+
1056+
world.entity_mut(e1).insert(Dense2);
1057+
1058+
assert_eq!(world.entity(e2).get::<Dense>().unwrap(), &Dense(1));
1059+
}
1060+
1061+
// regression test for https://github.com/bevyengine/bevy/pull/7805
1062+
#[test]
1063+
fn despawning_entity_updates_archetype_row() {
1064+
#[derive(Component, PartialEq, Debug)]
1065+
struct Dense(u8);
1066+
1067+
#[derive(Component)]
1068+
#[component(storage = "SparseSet")]
1069+
struct Sparse;
1070+
1071+
let mut world = World::new();
1072+
let e1 = world.spawn(Dense(0)).id();
1073+
let e2 = world.spawn(Dense(1)).id();
1074+
1075+
world.entity_mut(e1).insert(Sparse).remove::<Sparse>();
1076+
1077+
// archetype with [e2, e1]
1078+
// table with [e1, e2]
1079+
1080+
world.entity_mut(e2).despawn();
1081+
1082+
assert_eq!(world.entity(e1).get::<Dense>().unwrap(), &Dense(0));
1083+
}
1084+
1085+
// regression test for https://github.com/bevyengine/bevy/pull/7805
1086+
#[test]
1087+
fn despawning_entity_updates_table_row() {
1088+
#[derive(Component, PartialEq, Debug)]
1089+
struct Dense(u8);
1090+
1091+
#[derive(Component)]
1092+
#[component(storage = "SparseSet")]
1093+
struct Sparse;
1094+
1095+
let mut world = World::new();
1096+
let e1 = world.spawn(Dense(0)).id();
1097+
let e2 = world.spawn(Dense(1)).id();
1098+
1099+
world.entity_mut(e1).insert(Sparse).remove::<Sparse>();
1100+
1101+
// archetype with [e2, e1]
1102+
// table with [e1, e2]
1103+
1104+
world.entity_mut(e1).despawn();
1105+
1106+
assert_eq!(world.entity(e2).get::<Dense>().unwrap(), &Dense(1));
1107+
}
9101108
}

0 commit comments

Comments
 (0)