Skip to content

Fix panic-safety of eleven methods, and Deque's ring buffer indexing - #9

Open
tooson9010-spec wants to merge 6 commits into
teryror:mainfrom
tooson9010-spec:panic-safety
Open

Fix panic-safety of eleven methods, and Deque's ring buffer indexing#9
tooson9010-spec wants to merge 6 commits into
teryror:mainfrom
tooson9010-spec:panic-safety

Conversation

@tooson9010-spec

Copy link
Copy Markdown

Fixes the panic-safety issues from #8. While writing the tests I hit a
separate Deque bug that isn't in that issue — it's fixed here too, see
below.

Deque addresses the ring buffer without the front offset

Deque::truncate and retain compute physical slots as i % capacity,
but everywhere else in the type logical indices go through
physical_index, which is (front + i) % capacity. One push_front
makes front non-zero, and from then on both methods walk the wrong
slots — destroying memory that was never initialized, handing it to the
user's predicate as a &T, and leaving live elements alone. No panic
involved.

retain_respects_front builds a deque holding [10, 20, 30] and records
what the predicate sees; before the fix that's [20, 30, <garbage>].
truncate_respects_front ends the test process with SIGSEGV.

Panic-safety

Nine of the reported methods destroy values and only afterwards commit the
metadata that removes them from the collection. For ListMap and
PackedPool that's worse than a stale length, since their Drop impls
call clear and re-enter the loop they just unwound out of.

Vec::truncate, Deque::truncate, ListMap::clear, PackedPool::clear
and UnitCache::clear now commit first. Where the values are contiguous
they're dropped as a slice, so a panicking destructor doesn't leak the
ones behind it. PackedPool recycles its slots in a separate pass
beforehand — that part can't panic.

ListMap::remove and LruCache2::get_or_insert_with instead read the
value out of the slot before running anything user-controlled, which ends
the collection's claim on it right away.

DirectPool::try_insert_with_handle needed the opposite: it marked the
slot occupied before calling the closure that fills it, so it now runs the
closure first. Its test segfaults without the fix.

Deque::retain is the one case the predicate alone can break — ptr::copy
leaves the source valid, so a compacted element briefly lives in two slots
that are both inside the current length. It now keeps its progress in a
drop guard that compacts the unvisited tail back in on unwind.

Tests

Eleven, all in the existing mod tests blocks except cache.rs and
list_map.rs, which had none. Each was run against the unpatched code
first and fails there — inflated drop count, wrong value reaching user
code, or SIGSEGV.

test_utils::Droppable gets an opt-in panic_on_drop that disarms itself
before unwinding, so a double drop shows up as a count rather than an
abort. std comes in under cfg(test) for catch_unwind. 223 doc-tests
and the existing unit tests still pass.

An element's destructor may unwind. truncate committed the new length
only after destroying the tail, so an unwind left the vector claiming
ownership of already-destroyed elements, which Drop for Vec then
destroyed a second time.

Commit the length first and destroy the tail as a slice, matching
Drop for Vec and std's Vec::truncate. Destroying the tail as a slice
also keeps the remaining elements from leaking when one of them
unwinds.

Adds an opt-in panic_on_drop to test_utils::Droppable, and pulls in
std under cfg(test) for catch_unwind.
These four methods destroy values in place and commit the metadata that
removes them from the collection's logical state only afterwards. A
destructor is user-controlled and may unwind, and for ListMap and
PackedPool the Drop impl calls clear again, re-entering the same loop
over stale metadata.

ListMap::clear, PackedPool::clear: commit the emptied state first, then
destroy the values as a slice so a panicking destructor does not leak
the remaining ones. PackedPool recycles its slots in a separate loop
beforehand, as that part cannot panic.

ListMap::remove, LruCache2::get_or_insert_with, UnitCache::clear: move
the removed values out and finish updating the collection before
running their destructors, so an unwind cannot leave a slot both
uninitialized and claimed.
The method bumped the length and made the slot's generation counter odd
-- the pool's encoding of an occupied slot -- before calling the
user-supplied closure that produces the value. If the closure unwound,
the slot stayed uninitialized while the pool still claimed it, and Drop
for DirectPool destroyed that uninitialized memory.

Run the closure first and update the pool only once the value exists.
The free-list link is read out of the slot union before the value is
written over it.

Without the fix the added test terminates the process with SIGSEGV.
Both methods addressed the ring buffer with a bare 'i % capacity',
ignoring the 'front' offset that physical_index applies everywhere else.
With a non-zero front -- one push_front is enough -- they destroyed
slots that were never initialized, passed uninitialized memory to the
user's predicate, and left live elements untouched. No panic is needed
to reach this.

Both also committed the new length only after destroying or compacting,
so an unwinding destructor or predicate left the deque claiming values
it no longer owned.

truncate now translates logical indices through front, commits the
length first, and drops the removed range as one or two slices so a
panicking destructor does not leak the elements behind it.

retain translates its source and destination indices through front and
keeps its progress in a drop guard, which compacts the unvisited tail
back into the deque when the predicate or a destructor unwinds.
The previous version pushed to the front, which meant the indexing bug
diverted the traversal before any element was compacted -- the test
passed against the unfixed code. Keep the front at zero so the
predicate unwinds after two elements have been copied forward, which is
the state the fix is about.
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