name = "smallvec"
description = """
ETNA workload for the Rust `smallvec` crate. Each variant re-introduces one
historical bug-fix into a fresh patched branch and pairs it with a
framework-neutral property, four PBT adapters, and a deterministic witness
test.
"""
language = "rust"
strategies = ["proptest", "quickcheck", "crabcheck", "hegel"]
crate = "smallvec"
base_commit = "4ea9eb07cd598382f8c8aef448031f205376fa7a"

[[tasks]]
mutations = ["leak_inline_panic_3395246_1"]

[tasks.source]
repo = "https://github.com/servo/rust-smallvec"
commits = ["339524684344f74ea7e5e05b3b5ae4e5043999f8"]
commit_subjects = ["Fix: SmallVec::leak() to panic on inline storage (GHSA-5h7v-3586-wm8c)"]
summary = "`SmallVec::leak` handed back a `&mut [T]` borrowed from the consumed value's stack storage when the vector was still inline (GHSA-5h7v-3586-wm8c); the fix panics rather than returning a dangling reference."

[tasks.injection]
kind = "patch"
files = ["src/lib.rs"]
locations = [{ file = "src/lib.rs" }]
patch = "patches/leak_inline_panic_3395246_1.patch"

[tasks.bug]
short_name = "leak_inline_panic"
invariant = "`SmallVec::<T, N>::leak(self)` on an inline (not spilled) vector must panic. Returning a slice that borrows into the consumed stack frame would be unsound — any caller would receive a reference to freed storage."
how_triggered = "Removes the `if !self.spilled() { panic!(...) }` guard inside `leak`, so inline SmallVecs silently hand out a `&mut [T]` into the about-to-be-dropped stack buffer. The property builds an inline `SmallVec<u8, N>` via `SmallVec::new()` + `push`, runs `leak()` under `catch_unwind`, and reports `Fail` if the call returns `Ok`."

[[tasks.tasks]]
property = "LeakInlinePanics"
witnesses = [
  { test_fn = "witness_leak_inline_panics_case_inline_single_byte" },
]

[[tasks]]
mutations = ["append_set_len_1bd2dbc_1"]

[tasks.source]
repo = "https://github.com/servo/rust-smallvec"
commits = ["1bd2dbce81d9b58214bbbf5de30323017705d5a8"]
commit_subjects = ["bugfix for append not set self.len properly (#347)"]
summary = "`SmallVec::append` copied bytes into the destination buffer but forgot to call `set_len`, so after the call the logical length still reflected the pre-append state and the appended elements were invisible."

[tasks.injection]
kind = "patch"
files = ["src/lib.rs"]
locations = [{ file = "src/lib.rs" }]
patch = "patches/append_set_len_1bd2dbc_1.patch"

[tasks.bug]
short_name = "append_set_len"
invariant = "After `a.append(&mut b)`, `a.len()` equals `a.len() + b.len()` (pre-call) and every element is visible via iteration."
how_triggered = "Removes the trailing `unsafe { self.set_len(total_len) }` call in `SmallVec::append`, so the copy into the destination buffer happens but the logical length is never bumped. The property computes the expected concatenation length, compares against `va.len()`, and also checks that `va.iter().copied().collect::<Vec<_>>()` matches the concat — all three fail because `va.len()` stays at its pre-append value."

[[tasks.tasks]]
property = "AppendPreservesLength"
witnesses = [
  { test_fn = "witness_append_preserves_length_case_small_spilled" },
]

[[tasks]]
mutations = ["from_vec_zero_capacity_944f603_1"]

[tasks.source]
repo = "https://github.com/servo/rust-smallvec"
commits = ["944f60397a35043ef5704e9bf76b26cdcb5fc55d"]
commit_subjects = ["Correctly handle Vec with zero capacity in from_vec (#332)"]
summary = "`SmallVec::from_vec` wrapped a zero-capacity `Vec`'s dangling pointer directly into `RawSmallVec::new_heap` and tagged the result as spilled; the fix short-circuits to an empty inline SmallVec to avoid later UB in shrink/deallocate/push."

[tasks.injection]
kind = "patch"
files = ["src/lib.rs"]
locations = [{ file = "src/lib.rs" }]
patch = "patches/from_vec_zero_capacity_944f603_1.patch"

[tasks.bug]
short_name = "from_vec_zero_capacity"
invariant = "`SmallVec::from_vec(Vec::with_capacity(0))` must be inline (non-spilled). A zero-capacity `Vec` holds a dangling but non-null pointer; wrapping that pointer into a heap-tagged SmallVec produces an object that later UBs under `shrink_to_fit`, `deallocate`, or `push`."
how_triggered = "Removes the `if vec.capacity() == 0 { return Self::new(); }` early return, so `from_vec` forwards a dangling pointer into `RawSmallVec::new_heap` and tags the result as spilled. The property observes `sv.spilled() == true` on a SmallVec built from `Vec::with_capacity(0)` and fails immediately (without having to trigger the follow-on UB)."

[[tasks.tasks]]
property = "FromVecZeroCapacity"
witnesses = [
  { test_fn = "witness_from_vec_zero_capacity_case_empty_capacity_push_round_trip" },
]
