Skip to content

Fix #C-16: insert_by_ids accepts a documented-compliant short iterator - #25266

Open
kiana1kaslana wants to merge 1 commit into
bevyengine:mainfrom
kiana1kaslana:fix-insert-by-ids-soundness
Open

Fix #C-16: insert_by_ids accepts a documented-compliant short iterator#25266
kiana1kaslana wants to merge 1 commit into
bevyengine:mainfrom
kiana1kaslana:fix-insert-by-ids-soundness

Conversation

@kiana1kaslana

@kiana1kaslana kiana1kaslana commented Aug 2, 2026

Copy link
Copy Markdown

insert_by_ids is unsafe, but its Safety docs never said the iterator has to match the length of component_ids. The internal pairing uses zip, so a short iterator silently gets truncated while Table::allocate has already reserved all the columns. Reading them later is UB.

Three changes in crates/bevy_ecs/src/world/entity_access/:

  • Update the Safety docs on insert_by_ids to require an exact length match.
  • Update the Safety docs on insert_dynamic_bundle to say both iterators must match.
  • In insert_by_ids_internal, collect the iterator in debug builds and debug_assert_eq! its length against component_ids.len(). Gated on #[cfg(debug_assertions)], so release builds are unchanged.

Added two #[should_panic] tests in entity_access::mod.rs:

  • insert_by_ids_short_iterator_panics_in_debug (2 IDs, 1 component)
  • insert_by_ids_long_iterator_panics_in_debug (1 ID, 2 components)

All 71 existing entity_access tests still pass. cargo check -p bevy_ecs is clean in both debug and release.

The other two callers of insert_by_ids_internal (bundle::writer and entity::clone_entities) always pass matching lengths, so they're unaffected.

…tor length

`EntityWorldMut::insert_by_ids` is an `unsafe` API whose `# Safety` docs
did not state that `iter_components` must yield exactly
`component_ids.len()` items. Internally, `insert_dynamic_bundle` pairs
the two via `zip`, which silently truncates to the shorter iterator. If
the caller passes a short iterator, `Table::allocate` has already
reserved space for all components but only some are written — leaving
uninitialized memory that is later read as valid data (UB).

Changes:

1. **`insert_by_ids` Safety docs** — add the missing contract: the
   iterator must yield exactly `component_ids.len()` items. Fewer → UB
   from uninitialized memory; more → silently ignored.

2. **`insert_dynamic_bundle` Safety docs** — state that both iterators
   must yield the same number of items.

3. **`insert_by_ids_internal` debug guard** — in debug builds, eagerly
   collect `iter_components` into a `Vec` and `debug_assert_eq!` its
   length against `component_ids.len()`. This catches the mismatch
   before any uninitialized memory is accessed. The check is removed
   entirely in release builds (zero overhead).

4. **Regression tests** — two `#[should_panic]` tests (debug-only)
   verifying that both short and long iterators trigger the assertion.
@kiana1kaslana kiana1kaslana changed the title Fix C-16: insert_by_ids soundness — document and debug-assert itera… Fix #C-16: insert_by_ids accepts a documented-compliant short iterator Aug 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant