Skip to content

insert() commits the new size before constructing, so a throwing element ctor causes a double destroy #68

Description

@martinus

make_uninitialized_space() raises size() and then returns raw storage for the caller to fill:

shift_right(p, data<D>() + s, p + count);
set_size<D>(s + count);   // committed before anything is constructed
return p;

If the caller's construction throws partway, the container is left claiming elements that were never built — or, worse, that were built and then destroyed by the cleanup inside std::uninitialized_fill_n. The destructor then destroys them a second time.

auto v = ankerl::svector<ThrowOnCopy, 8>();
v.emplace_back("aaa...");
v.emplace_back("bbb...");

ThrowOnCopy::budget = 1;                                  // the 2nd copy throws
v.insert(v.begin(), 3, ThrowOnCopy("ccc..."));
size=2 capacity=8
inserting 3 copies, the 2nd throws...
caught: copy
after: size=5
=================================================================
ERROR: AddressSanitizer: heap-use-after-free
  #6 ThrowOnCopy::~ThrowOnCopy() f3.cpp:17
  #7 std::_Destroy<ThrowOnCopy>(ThrowOnCopy*)

size() is 5, but slot 0 and 1 hold destroyed objects. Reading them is a use-after-free and the destructor double-frees.

Affects everything that goes through make_uninitialized_space():

  • insert(pos, count, value)
  • insert(pos, value) / insert(pos, T&&)
  • insert(pos, first, last) (forward iterators)
  • emplace(pos, ...)

std::vector gives the strong guarantee for a single-element insert and at least the basic guarantee for the rest; here even the basic guarantee is violated, since the container is left in a state that cannot be destroyed.

Suggestion

Commit the size only after the elements exist, the way emplace_back() already does:

// construct before updating the size, so a throwing constructor doesn't leave the
// vector claiming an element that was never built
auto& element = *new (static_cast<void*>(ptr)) T(std::forward<Args>(args)...);
set_size(s + 1);

That means make_uninitialized_space() has to hand back the gap without committing, and each caller commits once its fill succeeded — plus cleaning up the shifted tail on the way out if it wants more than the basic guarantee.

Reproduced with g++ 16, -O1 -fsanitize=address, libstdc++.

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