Repository navigation
Fix double-drop when a component's Drop panics - #471
Conversation
Drop for Archetype calls clear, so if a component's Drop unwinds before self.len = 0 runs, the archetype is destroyed a second time. Take the length first; components not reached are leaked, which is safe.
run_on wipes the component list only after the loop, so a panicking command leaves components owned both by the world and the buffer. Track how many were consumed and split the two groups in a guard.
Is this still true when passing |
|
Also, for easier review, please squash changes from feedback into the commits that were changed. |
910e265 to
f4c118e
Compare
|
Sorry again for the slow reply. You're right, it isn't. The tests reset the counters at the start now, and the Also squashed the feedback commits into the ones they changed. |
remove destroys the component at index and backfills it from last, leaving a duplicate until self.len is committed. A panicking destructor skips that commit, so the removed slot and the duplicate both stay live. Do all the backfilling in a guard and keep only the destructors on the straight-line path, so the length is committed on both exits. Components of the removed entity that a panic did not reach are overwritten by the backfill and leaked, which is safe. Tests reset the thread-local counters at the start, since --test-threads=1 runs every test on the same thread. They use thread-locals rather than an Arc because the leak these paths produce is deliberate and Miri reports a leaked Arc as an error.
f4c118e to
50a34fb
Compare
You're fine, we're all volunteers here, participating as time permits. |
A panicking Drop during World::insert left already-moved components to be destroyed again by clear. Commit consumed first; regression test added.
6d6faa9 to
3f37a91
Compare
|
@Ralith |
|
Sure, if you like. It's difficult to imagine a realistic scenario in which this could be deliberately triggered and present an actual security hazard, so the severity should be recorded accordingly, but it's certainly not technically impossible. |
Found while researching panic-safety in Rust containers.
Summary
Three places destroy or move components before committing the state that says
which slots are live.
Componenthas no bound excluding a panickingDrop, soa caller only needs to catch the unwind and touch the container again. All three
are double-drops (CWE-415) reachable from safe Rust.
Archetype::clearself.len = 0Drop for ArchetypecallsclearagainArchetype::removeself.len = lastcopy_nonoverlappingCommandBuffer::run_onself.components.clear()CommandBuffer::cleardrops components already moved into the worldrun_onalready carries a comment saying "Wipe out component references socleardoesn't try to double-free". It just does not run when a commandunwinds.
Fix
cleartakes the length before running any destructor. Components not reachedare leaked, which is safe.
removeruns the loop under a guard. On unwind the guard finishes the backfillfor the remaining types without running further destructors, moves the entity
id, and commits the length. The dropped slot is overwritten by the moved entity
and the duplicate falls out of range, so the archetype is left consistent.
Components of the removed entity past the panic are leaked.
run_ontracks how many components the world has taken and splits the twogroups in a guard. Those already handed over are dropped from the list without
running destructors, the rest are destroyed normally. No leak on this path.
World::despawnis not covered. It updates its entity index from the valueremovereturns, and that is skipped when the unwind passes through, so themoved entity keeps a stale index entry. That is a logical inconsistency, not
memory unsafety. I left it alone since the right shape looks like your call.
Verification
Three regression tests count destructor invocations. Against the unpatched
code
clearruns 7 destructors where 4 components exist,run_onruns 3where 2 exist, and
despawnruns 8 where 6 exist. With the fixcleardestroys 3 of the 4 and
run_on2 of 2; thedespawntest asserts onlythat the count stays at or below 6, since how far the loop gets before the
unwind depends on the order the archetype stores its types in.
The double-frees were confirmed earlier with AddressSanitizer (
attempting double-free) on the unpatched code, throughWorld::clear,World::despawn,World::spawn_atandWorld::spawn_column_batch_at.Existing tests pass and
cargo build --no-default-featuresstill builds.Confirmed on 0.11.1.