guard against arbitrary constructions

This commit is contained in:
Elliott Pierce 2025-05-31 12:12:07 -04:00
parent 0c1c9c3fa4
commit 5a69ebfbc0
4 changed files with 68 additions and 34 deletions

View File

@ -1702,7 +1702,11 @@ impl<'w> BundleSpawner<'w> {
}
/// # Safety
/// `entity` must be allocated and have no location. `T` must match this [`BundleInfo`]'s type
/// `T` must match this [`BundleInfo`]'s type
///
/// # Panics
///
/// Panics if the entity has already been constructed.
#[inline]
#[track_caller]
pub unsafe fn construct<T: DynamicBundle>(
@ -1736,7 +1740,16 @@ impl<'w> BundleSpawner<'w> {
InsertMode::Replace,
caller,
);
entities.declare(entity.row(), Some(location));
let was_at = entities.declare(entity.row(), Some(location));
// We need to assert here.
// Even if we can ensure that this entity is fresh from an allocator,
// it does not prevent users constructing arbitrary rows, which may overlap with the allocator.
// One alternative would be making `Entity` creation unsafe, but this is a good safety net anyway.
assert!(
was_at.is_none(),
"Constructing an {entity} that was already constructed is not allowed."
);
entities.mark_construct_or_destruct(entity.row(), caller, self.change_tick);
(location, after_effect)
};

View File

@ -827,30 +827,40 @@ impl Entities {
/// Updates the location of an [`EntityRow`].
/// This must be called when moving the components of the existing entity around in storage.
/// Returns the previous location of the row.
///
/// # Safety
/// - The current location of the `row` must already be set. If not, try [`declare`](Self::declare).
/// - `location` must be valid for the entity at `row` or immediately made valid afterwards
/// before handing control to unknown code.
#[inline]
pub(crate) unsafe fn update(&mut self, row: EntityRow, location: EntityIdLocation) {
pub(crate) unsafe fn update(
&mut self,
row: EntityRow,
location: EntityIdLocation,
) -> EntityIdLocation {
// SAFETY: Caller guarantees that `row` already had a location, so `declare` must have made the index valid already.
let meta = unsafe { self.meta.get_unchecked_mut(row.index() as usize) };
meta.location = location;
mem::replace(&mut meta.location, location)
}
/// Declares the location of an [`EntityRow`].
/// This must be called when constructing/spawning entities.
/// Returns the previous location of the row.
///
/// # Safety
/// - `location` must be valid for the entity at `index` or immediately made valid afterwards
/// before handing control to unknown code.
#[inline]
pub(crate) unsafe fn declare(&mut self, row: EntityRow, location: EntityIdLocation) {
pub(crate) unsafe fn declare(
&mut self,
row: EntityRow,
location: EntityIdLocation,
) -> EntityIdLocation {
self.ensure_row(row);
// SAFETY: We just did `ensure_row`
let meta = unsafe { self.meta.get_unchecked_mut(row.index() as usize) };
meta.location = location;
mem::replace(&mut meta.location, location)
}
/// Ensures row is valid.

View File

@ -2440,7 +2440,8 @@ impl<'w> EntityWorldMut<'w> {
}
// SAFETY: Since we had a location, and it was valid, this is safe.
unsafe {
self.world.entities.update(self.entity.row(), None);
let was_at = self.world.entities.update(self.entity.row(), None);
debug_assert_eq!(was_at, Some(location));
self.world
.entities
.mark_construct_or_destruct(self.entity.row(), caller, change_tick);

View File

@ -1126,7 +1126,12 @@ impl World {
/// If the entity can not be constructed for any reason, returns an error.
///
/// If it succeeds, this declares the entity to have this bundle.
/// It is possible to construct an `entity` that has not been allocated yet.
///
/// Note: It is possible to construct an `entity` that has not been allocated yet;
/// however, doing so is currently a bad idea as the allocator may hand out this entity row in the future, assuming it to be not constructed.
/// This would cause a panic.
///
/// Manual construction is a powerful tool, but must be used carefully.
#[track_caller]
pub fn construct<B: Bundle>(
&mut self,
@ -1143,16 +1148,15 @@ impl World {
caller: MaybeLocation,
) -> Result<EntityWorldMut<'_>, ConstructionError> {
self.entities.validate_construction(entity)?;
// SAFETY: We just ensured it was valid.
Ok(unsafe { self.construct_unchecked(entity, bundle, caller) })
Ok(self.construct_unchecked(entity, bundle, caller))
}
/// Constructs `bundle` on `entity`.
///
/// # Safety
/// # Panics
///
/// `entity` must be valid and have no location.
pub(crate) unsafe fn construct_unchecked<B: Bundle>(
/// Panics if the entity row is already constructed
pub(crate) fn construct_unchecked<B: Bundle>(
&mut self,
entity: Entity,
bundle: B,
@ -1193,32 +1197,38 @@ impl World {
caller: MaybeLocation,
) -> Result<EntityWorldMut<'_>, ConstructionError> {
self.entities.validate_construction(entity)?;
// SAFETY: We just ensured it was valid.
Ok(unsafe { self.construct_empty_unchecked(entity, caller) })
Ok(self.construct_empty_unchecked(entity, caller))
}
/// Constructs `bundle` on `entity`.
///
/// # Safety
/// # Panics
///
/// `entity` must be valid and have no location.
pub(crate) unsafe fn construct_empty_unchecked(
/// Panics if the entity row is already constructed
pub(crate) fn construct_empty_unchecked(
&mut self,
entity: Entity,
caller: MaybeLocation,
) -> EntityWorldMut<'_> {
let archetype = self.archetypes.empty_mut();
// PERF: consider avoiding allocating entities in the empty archetype unless needed
let table_row = self.storages.tables[archetype.table_id()].allocate(entity);
// SAFETY: no components are allocated by archetype.allocate() because the archetype is
// empty
let location = unsafe { archetype.allocate(entity, table_row) };
let change_tick = self.change_tick();
self.entities.declare(entity.row(), Some(location));
self.entities
.mark_construct_or_destruct(entity.row(), caller, change_tick);
// SAFETY: Locations are immediately made valid
unsafe {
let archetype = self.archetypes.empty_mut();
// PERF: consider avoiding allocating entities in the empty archetype unless needed
let table_row = self.storages.tables[archetype.table_id()].allocate(entity);
// SAFETY: no components are allocated by archetype.allocate() because the archetype is
// empty
let location = archetype.allocate(entity, table_row);
let change_tick = self.change_tick();
let was_at = self.entities.declare(entity.row(), Some(location));
assert!(
was_at.is_none(),
"Attempting to construct an empty entity, but it was already constructed."
);
self.entities
.mark_construct_or_destruct(entity.row(), caller, change_tick);
EntityWorldMut::new(self, entity, Some(location))
EntityWorldMut::new(self, entity, Some(location))
}
}
/// Spawns a new [`Entity`] with a given [`Bundle`] of [components](`Component`) and returns
@ -1292,8 +1302,8 @@ impl World {
caller: MaybeLocation,
) -> EntityWorldMut {
let entity = self.spawn_null();
// SAFETY: This was just spawned from null.
unsafe { self.construct_unchecked(entity, bundle, caller) }
// This was just spawned from null, so it shouldn't panic.
self.construct_unchecked(entity, bundle, caller)
}
/// Spawns a new [`Entity`] and returns a corresponding [`EntityWorldMut`], which can be used
@ -1327,9 +1337,9 @@ impl World {
}
pub(crate) fn spawn_empty_with_caller(&mut self, caller: MaybeLocation) -> EntityWorldMut {
let entity = self.allocator.alloc();
// SAFETY: entity was just allocated
unsafe { self.construct_empty_unchecked(entity, caller) }
let entity = self.spawn_null();
// This was just spawned from null, so it shouldn't panic.
self.construct_empty_unchecked(entity, caller)
}
/// Spawns a batch of entities with the same component [`Bundle`] type. Takes a given