Skip to content

Comments

Fix unsafe access of component metadata (change ticks) in bevy_ecs::storage::Table#23065

Open
nils-mathieu wants to merge 3 commits intobevyengine:mainfrom
nils-mathieu:main
Open

Fix unsafe access of component metadata (change ticks) in bevy_ecs::storage::Table#23065
nils-mathieu wants to merge 3 commits intobevyengine:mainfrom
nils-mathieu:main

Conversation

@nils-mathieu
Copy link

@nils-mathieu nils-mathieu commented Feb 20, 2026

Objective

This pull requests simply changes three functions in bevy_ecs/src/storage/table/mod.rs which currently may incorrectly access an out of bound element.

Currently those functions use the following pattern to guard against invalid TableRow:

/// Get the specific [`change tick`](Tick) of the component matching `component_id` in `row`.
pub fn get_changed_tick(
  &self,
  component_id: ComponentId,
  row: TableRow,
) -> Option<&UnsafeCell<Tick>> {
  (row.index_u32() < self.entity_count()).then_some(
    // SAFETY: `row.as_usize()` < `len`
    unsafe {
      self.get_column(component_id)?
        .changed_ticks
        .get_unchecked(row.index())
    },
  )
}

The usage of then_some is invalid here because it takes its input by value, which evaluates the get_unchecked call instantly even if row.index_u32() < self.entity_count() is false.

Calling get_unchecked with an invalid index triggers instant undefined behavior even if the value is not used.

Solution

We can't really use then instead of then_some because self.get_column can fail by returning None, so I opted for a regular if guard like this:

/// Get the specific [`change tick`](Tick) of the component matching `component_id` in `row`.
pub fn get_changed_tick(
  &self,
  component_id: ComponentId,
  row: TableRow,
) -> Option<&UnsafeCell<Tick>> {
  if row.index_u32() >= self.entity_count() {
    return None;
  }

  // SAFETY: `row.as_usize()` < `len`
  self.get_column(component_id)
    .map(|col| unsafe { col.changed_ticks.get_unchecked(row.index()) })
}

Testing

I tried to run the CI on the whole thing but my version of clippy (nightly) is triggered by other parts of the code and I was too lazy to install Rust stable and re-compile everything again. I will fix anything that fails in CI here and re-check everything locally if it turns out to fail.

Other Notes

I'm not familiar with the codebase (well, I've been reading an poking around for a bit but I don't know everything), so there might be other places in the code with the same issue. I grep'ed then_some and found no other instance of the same problem, so at least it doesn't appear anywhere else in this form.

@github-actions
Copy link
Contributor

Welcome, new contributor!

Please make sure you've read our contributing guide and we look forward to reviewing your pull request shortly ✨

@nils-mathieu
Copy link
Author

Not sure why the macos build times out. Is that an issue on the runner's end?

@kfc35 kfc35 added C-Bug An unexpected or incorrect behavior A-ECS Entities, components, systems, and events D-Straightforward Simple bug fixes and API improvements, docs, test and examples D-Unsafe Touches with unsafe code in some way S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Feb 21, 2026
@github-project-automation github-project-automation bot moved this to Needs SME Triage in ECS Feb 21, 2026
Copy link
Contributor

@kfc35 kfc35 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work! Appreciate the easy-to-understand PR description. Thanks for your first contribution 🙏

Copy link
Member

@mockersf mockersf left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this have an impact on performances? Maybe not run the whole benchmarks, but could you check the code generated for costly differences?

@nils-mathieu
Copy link
Author

nils-mathieu commented Feb 21, 2026

@mockersf

Could this have an impact on performances? Maybe not run the whole benchmarks, but could you check the code generated for costly differences?

I would naively expect the "success" case to have the same performance as it does the same things (check row index + access), and the "failure" case the have better performance because we're no longer performing the access at all in that case.

I'm not sure how to check the generated code though.

That being said, I suspect that the compiler was actually doing the following:

  1. Check if the table has a column for component_id. If not bail out correctly.
  2. See the get_unchecked which requires row < len to be sound.
  3. Conclude that row < len must always be true and that therefore row.index_u32() < self.entity_count() is redundant.
  4. Remove row.index_u32() < self.entity_count() entirely and only keep self.get_column(component_id).map(|col| unsafe { col.changed_ticks.get_unchecked(row.index()) })

If it was doing that, then this would technically be a regression performance-wise? I would be surprised if it's even measurable

@kfc35 kfc35 added S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it and removed S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Feb 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-ECS Entities, components, systems, and events C-Bug An unexpected or incorrect behavior D-Straightforward Simple bug fixes and API improvements, docs, test and examples D-Unsafe Touches with unsafe code in some way S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it

Projects

Status: Needs SME Triage

Development

Successfully merging this pull request may close these issues.

4 participants