Skip to content

Panic safe component drop#24393

Open
SpecificProtagonist wants to merge 13 commits into
bevyengine:mainfrom
SpecificProtagonist:panic-safe-component-drop
Open

Panic safe component drop#24393
SpecificProtagonist wants to merge 13 commits into
bevyengine:mainfrom
SpecificProtagonist:panic-safe-component-drop

Conversation

@SpecificProtagonist
Copy link
Copy Markdown
Contributor

@SpecificProtagonist SpecificProtagonist commented May 22, 2026

Objective

Fixes #2860

bevy_ecs is not panic-safe in regards to dropping components. Note that #12929 has since made some cases safe by permanently disabling the drop function in some cases, but others remained.

See also #20368 for lack of panic safety in required component constructors. The fix (#20933) to that differs a bit in that a panic in a required component constructor means we have to move the entity to a different archetype than originally planned, which is a bit more involved.

Solution

When a panic occurs while making changes that must be atomic (such as editing entity metadata or moving around component data), catch any panics originating from component drop functions and resume unwinding at the end.

While I've added a panics section to the doc comments of relevant internal functions, I have not done so for any of the safe wrappers.

Testing

Added the panic_in_component_drop test based on the linked issue.

@SpecificProtagonist SpecificProtagonist added C-Bug An unexpected or incorrect behavior A-ECS Entities, components, systems, and events P-Unsound A bug that results in undefined compiler behavior D-Unsafe Touches with unsafe code in some way S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels May 22, 2026
@github-project-automation github-project-automation Bot moved this to Needs SME Triage in ECS May 22, 2026
last_element_index: usize,
row: TableRow,
) {
self.added_ticks
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

If we have unchecked/unsafe code that we are touching, we should be trying to gradually wrapping these with unsafe blocks, so we can eventually enable the unsafe_op_in_unsafe_fn lint.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This PR doesn't really affect existing safety requirements, so adding extra unsafe blocks around existing unsafe code would just make this PR harder to review. I'll do a separate PR for more unsafe_op_in_unsafe_fn though :)

Copy link
Copy Markdown
Contributor

@chescock chescock left a comment

Choose a reason for hiding this comment

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

Oh, nice! This will good to fix!

This is tricky enough code that I want to review it a little more carefully before approving than I have time for right now, but I wanted to leave a few thoughts.

Comment thread crates/bevy_ecs/src/bundle/info.rs Outdated
insert_mode: InsertMode,
caller: MaybeLocation,
) {
) -> Option<Box<dyn Any + Send>> {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'd vote for using Result<(), Box<dyn Any + Send>> here to make it more clear that the payload represents an error case.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I don't have a strong opinion here; changed (in all places).

Comment thread crates/bevy_ecs/src/bundle/insert.rs Outdated
new_location,
sparse_sets,
move_result.new_table,
move_result.panic,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Making sure I understand: A panic here isn't actually possible, right? move_row can only return a panic if it drops a value, which it only does if the new table is missing a column, which will only be the case during a remove and never during an insert. But we have to handle that case anyway, and an assert! would just introduce a new way to panic.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yep, good catch. Got lost in the weeds and when I came back here I just saw the DROP=true. I've removed the check and added a comment.

Comment thread crates/bevy_ecs/src/bundle/writer.rs
self.changed_by.as_mut().map(|changed_by| {
changed_by.swap_remove_unchecked_nonoverlapping(row.index(), last_element_index);
});
self.data
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The reordering here is subtle! Would you add a comment noting that this needs to be last in case drop panics (and similarly in the other methods)? I can imagine a future developer adding more things after this line without thinking about panic safety.

Hmm, even then, this seems really hard to get right. Maybe we could try to restructure this so that it doesn't depend on the order?

One approach might be to always put the catch_unwind directly around the call to drop, and propagate the panic payload explicitly as a Result or Option. So, BlobArray::swap_remove_and_drop_unchecked_nonoverlapping would return the panic payload, and the order of these lines wouldn't matter. That should also make it easier to verify that every call to drop has a catch_unwind, since they'd always be together.

Another approach might be to move the drop calls to happen later. If we do all the moves and swaps first, and then do all the drop calls together at the end, then a panic that escapes from a drop won't break any of our invariants. One downside of that approach is that we'd need to keep the values around longer, so we wouldn't be able to replace swaps with moves in more places like you did in #24312.

Comment thread crates/bevy_ecs/src/schedule/executor/single_threaded.rs Outdated
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-Unsafe Touches with unsafe code in some way P-Unsound A bug that results in undefined compiler behavior S-Needs-Review Needs reviewer attention (from anyone!) to move forward

Projects

Status: Needs SME Triage

Development

Successfully merging this pull request may close these issues.

Panic in drop leads to a double drop on spawning a new entity

4 participants