Skip to content

Commit e9e6299

Browse files
james7132SkiFire13
andauthored
Use the capacity from the entities Vec to initialize Table columns (#20528)
# Objective When working with `realloc`, it's a safety invariant to pass in the existing layout of the allocation that is being reallocated. This may not be the case with newly created `Table`s. `Vec::with_capacity`'s documentation states that it will return an allocation with enough space for *at least* `capacity` elements, not exactly `capacity`. This means that `entities.capacity()` may be greater than the provided capacity. As the `ThinColumn`s use this as their capacity, the new Layout fed to `realloc` will not match the allocation originally provided to `alloc`. This is unsound. While investigating this, I also found that we were not validating that the total capacity of `BlobArray`'s layout upon reallocation were less than `isize::MAX` via `array_layout_unchecked`. ## Solution Begin `Table` construction by allocating the `entities` Vec, and use it's capacity to allocate the columns instead of directly feeding the provided capacity into `ThinColumn::with_capacity`. Replace the `array_layout_unchecked` call with a safe call to `array_layout`, and panic if it fails. ## Testing Tested this locally against existing unit tests and miri. --------- Co-authored-by: Giacomo Stevanato <[email protected]>
1 parent 56c4100 commit e9e6299

File tree

2 files changed

+25
-7
lines changed

2 files changed

+25
-7
lines changed

crates/bevy_ecs/src/storage/blob_array.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -258,9 +258,8 @@ impl BlobArray {
258258
#[cfg(debug_assertions)]
259259
debug_assert_eq!(self.capacity, current_capacity.get());
260260
if !self.is_zst() {
261-
// SAFETY: `new_capacity` can't overflow usize
262-
let new_layout =
263-
unsafe { array_layout_unchecked(&self.item_layout, new_capacity.get()) };
261+
let new_layout = array_layout(&self.item_layout, new_capacity.get())
262+
.expect("array layout should be valid");
264263
// SAFETY:
265264
// - ptr was be allocated via this allocator
266265
// - the layout used to previously allocate this array is equivalent to `array_layout(&self.item_layout, current_capacity.get())`

crates/bevy_ecs/src/storage/table/mod.rs

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -132,17 +132,22 @@ impl TableRow {
132132
/// [`with_capacity`]: Self::with_capacity
133133
/// [`add_column`]: Self::add_column
134134
/// [`build`]: Self::build
135+
//
136+
// # Safety
137+
// The capacity of all columns is determined by that of the `entities` Vec. This means that
138+
// it must be the correct capacity to allocate, reallocate, and deallocate all columns. This
139+
// means the safety invariant must be enforced even in `TableBuilder`.
135140
pub(crate) struct TableBuilder {
136141
columns: SparseSet<ComponentId, ThinColumn>,
137-
capacity: usize,
142+
entities: Vec<Entity>,
138143
}
139144

140145
impl TableBuilder {
141146
/// Start building a new [`Table`] with a specified `column_capacity` (How many components per column?) and a `capacity` (How many columns?)
142147
pub fn with_capacity(capacity: usize, column_capacity: usize) -> Self {
143148
Self {
144149
columns: SparseSet::with_capacity(column_capacity),
145-
capacity,
150+
entities: Vec::with_capacity(capacity),
146151
}
147152
}
148153

@@ -151,7 +156,7 @@ impl TableBuilder {
151156
pub fn add_column(mut self, component_info: &ComponentInfo) -> Self {
152157
self.columns.insert(
153158
component_info.id(),
154-
ThinColumn::with_capacity(component_info, self.capacity),
159+
ThinColumn::with_capacity(component_info, self.entities.capacity()),
155160
);
156161
self
157162
}
@@ -161,7 +166,7 @@ impl TableBuilder {
161166
pub fn build(self) -> Table {
162167
Table {
163168
columns: self.columns.into_immutable(),
164-
entities: Vec::with_capacity(self.capacity),
169+
entities: self.entities,
165170
}
166171
}
167172
}
@@ -178,6 +183,11 @@ impl TableBuilder {
178183
/// [structure-of-arrays]: https://en.wikipedia.org/wiki/AoS_and_SoA#Structure_of_arrays
179184
/// [`Component`]: crate::component::Component
180185
/// [`World`]: crate::world::World
186+
//
187+
// # Safety
188+
// The capacity of all columns is determined by that of the `entities` Vec. This means that
189+
// it must be the correct capacity to allocate, reallocate, and deallocate all columns. This
190+
// means the safety invariant must be enforced even in `TableBuilder`.
181191
pub struct Table {
182192
columns: ImmutableSparseSet<ComponentId, ThinColumn>,
183193
entities: Vec<Entity>,
@@ -529,6 +539,11 @@ impl Table {
529539
/// Allocate memory for the columns in the [`Table`]
530540
///
531541
/// The current capacity of the columns should be 0, if it's not 0, then the previous data will be overwritten and leaked.
542+
///
543+
/// # Safety
544+
/// The capacity of all columns is determined by that of the `entities` Vec. This means that
545+
/// it must be the correct capacity to allocate, reallocate, and deallocate all columns. This
546+
/// means the safety invariant must be enforced even in `TableBuilder`.
532547
fn alloc_columns(&mut self, new_capacity: NonZeroUsize) {
533548
// If any of these allocations trigger an unwind, the wrong capacity will be used while dropping this table - UB.
534549
// To avoid this, we use `AbortOnPanic`. If the allocation triggered a panic, the `AbortOnPanic`'s Drop impl will be
@@ -544,6 +559,10 @@ impl Table {
544559
///
545560
/// # Safety
546561
/// - `current_column_capacity` is indeed the capacity of the columns
562+
///
563+
/// The capacity of all columns is determined by that of the `entities` Vec. This means that
564+
/// it must be the correct capacity to allocate, reallocate, and deallocate all columnts. This
565+
/// means the safety invariant must be enforced even in `TableBuilder`.
547566
unsafe fn realloc_columns(
548567
&mut self,
549568
current_column_capacity: NonZeroUsize,

0 commit comments

Comments
 (0)