Skip to content

Panic-safety unsoundness in several collections (double-free / use-after-free) #1

Description

@tooson9010-spec

Hello — while researching panic-safety in Rust crates, I found that several
public APIs in fastvec 2.0.0 are not panic-safe. Each is reachable from safe
Rust and leads to a double-free (CWE-415) / use-after-free (CWE-416).

There are two root causes.

Destruction before the length commit

clear and truncate destroy elements with drop_in_place and set the length
metadata only afterwards (self.len, or len_and_flag for SmallVec). An
element's Drop is user-controlled and may panic; if it does, the length update
is skipped and the container keeps a stale length that still identifies
already-destroyed elements as live. When the container is later dropped, its Drop
trusts the stale length and destroys those elements a second time.

Representative shape:

ptr::drop_in_place(elements); // T::drop() may panic
self.len = new_len;           // skipped on unwind -> stale len

Affected methods (this class):

  • FastVecData::clear
  • FastVecData::truncate
  • ArrayVec::clear
  • ArrayVec::truncate
  • SmallVec::clear
  • SmallVec::truncate

(The FastVecData methods are reachable through FastVec::data().)

Non-destructive compaction before the length commit (ArrayVec::retain_mut)

ArrayVec::retain_mut iterates 0..self.len directly, compacts kept elements
toward the front with ptr::copy, and commits self.len only after the loop —
with no pre-loop guard. It has two panic points:

  • A removed element's Drop panics at drop_in_place (the first class above).
  • The predicate f panics after a compaction. ptr::copy does not invalidate the
    source slot, so once a kept element is compacted the same value exists in two
    slots both inside 0..self.len; if f then unwinds, the duplicate stays live
    and is dropped a second time. In this path no element Drop needs to panic —
    the predicate alone is enough (unwrap(), indexing, assert!).
for index in 0..self.len {
    let dst = base_ptr.add(index);
    if f(&mut *dst) {                          // predicate may unwind
        ptr::copy(dst, base_ptr.add(count), 1); // duplicates the value
        count += 1;
    } else {
        ptr::drop_in_place(dst);                // element Drop may panic
    }
}
self.len = count;                               // skipped on unwind

Affected methods:

  • ArrayVec::retain_mut (src/array.rs:706)
  • ArrayVec::retain (delegates to retain_mut)

Two things worth noting for retain_mut:

  • ArrayVec declares impl UnwindSafe for ArrayVec<T, N> where T: UnwindSafe
    (src/array.rs:64), which this behaviour contradicts — a panic in retain_mut
    leaves the container in a state that double-frees on drop.
  • The crate already guards its other retain_mut implementations by setting the
    length to zero before the loop (self.len = 0; // Ensure safety if panicked),
    so the same guard applied here would fix it.

Trigger conditions

  • The first class: an element whose Drop can panic, plus a call to the affected
    method.
  • ArrayVec::retain_mut: any element that owns memory, plus a predicate that can
    panic. No panicking Drop is required.

All are reachable from safe Rust with unwinding panics and catch_unwind.

Affected versions

Confirmed on 2.0.0.

Thank you for your time.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions