Retain cleared allocations during unwinding

This commit is contained in:
2026-07-31 04:04:46 +02:00
parent be98362c28
commit e883282c80
3 changed files with 67 additions and 22 deletions
+5
View File
@@ -88,6 +88,11 @@ impl TypeErasedVec {
}
/// Clear any remaining elements in the vector, dropping them.
///
/// # Panics
///
/// If an element destructor panics, the vector remains valid and empty and
/// retains its allocation.
pub fn clear(&mut self) {
// SAFETY: The clear function correctly targets the currently stored type elements.
unsafe { (self.vtable.clear)(&mut self.parts) };
+42 -6
View File
@@ -323,23 +323,48 @@ fn test_reserve_is_unwind_safe() {
#[test]
fn test_clear_is_unwind_safe_when_element_drop_panics() {
struct PanicOnFirstDrop {
struct PanicOnSelectedDrop {
drop_count: Rc<Cell<usize>>,
has_panicked: Rc<Cell<bool>>,
should_panic: bool,
}
impl Drop for PanicOnFirstDrop {
impl Drop for PanicOnSelectedDrop {
fn drop(&mut self) {
let Some(next_count) = self.drop_count.get().checked_add(1) else {
panic!("drop counter overflow");
};
self.drop_count.set(next_count);
assert!(
self.has_panicked.replace(true),
!self.should_panic || self.has_panicked.replace(true),
"simulated panic while clearing an element"
);
}
}
let drop_count = Rc::new(Cell::new(0));
let has_panicked = Rc::new(Cell::new(false));
let mut erased = TypeErasedVec::new(vec![PanicOnFirstDrop {
has_panicked: Rc::clone(&has_panicked),
}]);
let values = vec![
PanicOnSelectedDrop {
drop_count: Rc::clone(&drop_count),
has_panicked: Rc::clone(&has_panicked),
should_panic: false,
},
PanicOnSelectedDrop {
drop_count: Rc::clone(&drop_count),
has_panicked: Rc::clone(&has_panicked),
should_panic: true,
},
PanicOnSelectedDrop {
drop_count: Rc::clone(&drop_count),
has_panicked: Rc::clone(&has_panicked),
should_panic: false,
},
];
let original_ptr = values.as_ptr();
let original_capacity = values.capacity();
let mut erased = TypeErasedVec::new(values);
let result = panic::catch_unwind(panic::AssertUnwindSafe(|| {
erased.clear();
@@ -347,7 +372,18 @@ fn test_clear_is_unwind_safe_when_element_drop_panics() {
assert!(result.is_err());
assert!(has_panicked.get());
assert_eq!(drop_count.get(), 3);
assert!(erased.is_empty());
assert_eq!(erased.capacity(), original_capacity);
{
// SAFETY: Clear retains the allocation's original element type.
let guard = unsafe { erased.cast_type::<PanicOnSelectedDrop>() };
assert_eq!(guard.as_slice().as_ptr(), original_ptr);
}
drop(erased);
assert_eq!(drop_count.get(), 3);
}
#[test]
+20 -16
View File
@@ -33,6 +33,14 @@ impl<'parts, T> VecPartsRestoreGuard<'parts, T> {
fn length(&self) -> usize {
self.vec.len()
}
fn capacity(&self) -> usize {
self.vec.capacity()
}
fn pointer(&self) -> *const T {
self.vec.as_ptr()
}
}
impl<T> Drop for VecPartsRestoreGuard<'_, T> {
@@ -61,23 +69,19 @@ impl TypeErasedVecVtable {
}
unsafe fn clear_vec<T>(parts: &mut VecParts) {
let original_parts = std::mem::replace(parts, VecParts::from_vec(Vec::<T>::new()));
let original_ptr = parts.ptr.as_ptr();
let original_capacity = parts.cap;
// SAFETY: We transferred ownership of the original allocation out of
// `parts` before reconstructing the Vec. If `clear` unwinds, the Vec
// owns and frees that allocation while `parts` remains a valid empty Vec.
unsafe {
let mut vec = original_parts.into_vec::<T>();
vec.clear();
let new_parts = VecParts::from_vec(vec);
debug_assert!(std::ptr::eq(
new_parts.ptr.as_ptr(),
original_parts.ptr.as_ptr()
));
debug_assert_eq!(new_parts.cap, original_parts.cap);
debug_assert_eq!(new_parts.len, 0);
*parts = new_parts;
}
// SAFETY: This vtable function is selected only for raw parts that
// were produced by Vec<T>.
let mut restore_guard = unsafe { VecPartsRestoreGuard::<T>::new(parts) };
restore_guard.vec_mut().clear();
debug_assert!(std::ptr::eq(
restore_guard.pointer().cast::<u8>(),
original_ptr
));
debug_assert_eq!(restore_guard.capacity(), original_capacity);
debug_assert_eq!(restore_guard.length(), 0);
}
unsafe fn reserve_vec<T>(parts: &mut VecParts, additional: usize) {