[core] Fix resource leak in table hooks for types without move_ctor#2047
Open
Indra-db wants to merge 2 commits intoSanderMertens:masterfrom
Open
[core] Fix resource leak in table hooks for types without move_ctor#2047Indra-db wants to merge 2 commits intoSanderMertens:masterfrom
Indra-db wants to merge 2 commits intoSanderMertens:masterfrom
Conversation
for types without move_ctor When a language binding (e.g. Rust) registers move_dtor and ctor_move_dtor but not move_ctor, three code paths in table management could leak resources: flecs_table_delete: when deleting a non-last entity (destruct=true), the fallback condition (!move_ctor && ctor_move_dtor) always selected ctor_move_dtor, which treats dst as uninitialized and skips dropping the deleted entity's data. Fixed by gating the fallback on !destruct, so move_dtor is used when dst holds live data. flecs_table_move (interleaved and trailing column loops): when removing a component from a non-last entity, use_move_dtor=false caused the dtor to be skipped entirely for the removed component. Fixed by forcing dtor=true when move_ctor is not set but ctor_move_dtor is, ensuring immediate cleanup rather than deferring to table compaction. Added three tests to ComponentLifecycle that validate each code path and fail if the corresponding fix is reverted.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Types that register
move_dtorandctor_move_dtorbut notmove_ctorleak resources during table compaction and component removal. This affects:dtor,move_dtor,ctor_move_dtor, leavemoveandmove_ctorNULL)lifecycle_traits.hppreturnsnullptrformove_ctorwhenis_trivially_move_constructible, but still registersctor_move_dtorbecause the dtor is non-trivial)Root cause:
ctor_move_dtortreats dst as uninitialized (placement new), whilemove_dtordrops dst before overwriting. When dst holds live data,ctor_move_dtorskips the drop and leaks.Changes
flecs_table_delete: added!destructguard to thector_move_dtorfallback. Whendestruct=true(entity deletion), dst holds live data, somove_dtormust be used. The fallback toctor_move_dtoris only correct whendestruct=false(table compaction after a move, where dst was already moved away).flecs_table_move(interleaved and trailing column loops): forcedtor=truewhen!move_ctor && ctor_move_dtorfor removed components. Previously,use_move_dtor=false(non-last entity) caused the dtor to be skipped entirely, deferring cleanup to table compaction. For types withoutmove_ctor, compaction does not properly clean up the removed data.Which types are affected
move_ctorctor_move_dtorHook selection design
For types where
move_ctor=NULLandctor_move_dtor!=NULL:flecs_table_delete, non-last,destruct=truemove_dtorflecs_table_delete, non-last,destruct=falsector_move_dtorflecs_table_move, matching columnsctor_move_dtor(existing logic)flecs_table_move, removed column, last rowinvoke_remove_hooks(dtor=true)flecs_table_move, removed column, not last rowEdge cases
All scenarios assume
move_ctor=NULL,ctor_move_dtor!=NULL,move_dtor!=NULL.Fixed by this PR
ecs_deleteon non-last entity (destruct=true)ecs_delete->flecs_table_delete(world, table, row, true). Last entity moves into gap. Before fix:!move_ctor && ctor_move_dtoralways true,ctor_move_dtorused, dst leaked. After fix:!destructis false, falls through tomove_dtor, dst dropped first. Correct.ecs_add/ecs_removetable move, entity is NOT last in source, removed columnsuse_move_dtor=false. Before fix:dtor=false, dtor skipped, data abandoned. Compaction (flecs_table_delete(destruct=false)) usedctor_move_dtoron the slot, which treats it as uninitialized, removed component data leaked. After fix:dtor = false || (!move_ctor && ctor_move_dtor) = true, dtor fires immediately. Correct. Matching columns and compaction behavior unchanged (both already correct).Not affected by this PR
ecs_deleteon last entity - hitsrow == countbranch, callsinvoke_remove_hooks(dtor=true), no move hooks involved.ecs_add/ecs_removetable move, entity is last in source -use_move_dtor=truealready handled all paths correctly.ecs_addwith no components removed - removed-column loops don't execute.Types with
move_ctorset -!move_ctoris false, none of the new conditions trigger.Fully trivial types (no hooks) - fast path (memcpy), no hook logic reached.
ctor_move_dtor=NULLandmove_ctor=NULL- condition evaluates totrue && false= false, falls through tomove_dtoror memcpy.Mixed hook configurations on same entity - each component processed independently per column.
Deferred operations - same call chain (
flecs_move_entity->flecs_table_move->flecs_table_delete), same fix applies.move_dtor=NULLbutctor_move_dtorset -flecs_type_info_move_dtorfalls back tomove+ dtor or memcpy. Not a realistic configuration: both Rust and C++ bindings always setmove_dtoralongsidector_move_dtor.Tests
Eight new tests in
ComponentLifecyclecovering three configurations.Tests that fail without the fix:
delete_nonlast_with_destruct_no_move_ctorremove_component_nonlast_no_move_ctor_calls_dtor_trailingremove_component_nonlast_no_move_ctor_calls_dtor_interleaveddelete_nonlast_trivial_move_with_dtor_move_dtor_calledremove_component_nonlast_trivial_move_with_dtor_dtor_calledTests that pass both with and without the fix:
delete_last_no_move_ctordelete_nonlast_nontrivial_move_ctor_move_dtor_not_affectedremove_component_nonlast_nontrivial_move_ctor_not_affectedAlso validated against a few hundred lifecycle test cases on the Flecs-Rust side.