Why can't I "destroy" CRTP vector that is "self-owned" but still can deallocate its address?

77 Views Asked by At

From Björn Fahller's Ligthning Talk at CPP Meeting 2023. => youtu.be/LKKmPAQFNgE

It's about how one can force c++ to leak memory without touching new or even malloc.

    struct V : vector<V> {};
    auto v = V{};
    v.emplace_back();
    v.swap(v.front());  // v representation becomes uint64_t[3]{ 0x0, 0x0, 0x0},
                        // so the vector allocation gets lost because no refs are left on the scope.

So I wonder if I can manually destroy it.

    struct V : vector<V> {};
    auto v = V{};
    v.emplace_back();
    v.emplace_back();
    v.emplace_back();
    v.emplace_back();

    auto front = v.front();

    v.swap(v.front());
    
    using allocator = std::allocator<V>;
    using atraits = std::allocator_traits<allocator>;
    auto a = front.get_allocator();
    atraits::destroy(a, &front + 1);   // Ok
    atraits::destroy(a, &front + 2);   // Ok
    atraits::destroy(a, &front + 3);   // Ok
    //  atraits::destroy(a, &front);   // error SIGSEGV
    atraits::deallocate(a, &front, 4); // still seems Ok?

SIGSEGV occurs when trying to destroy V object that owns it own address.

0x1796320 : 0x1796320          (alloc_begin_ptr)  // It owns itself!!!
0x1796328 : 0x1796380 (one_pass_content_end_ptr)
0x1796330 : 0x1796380   (one_pass_alloc_end_ptr)

0x1796338 : 0x0          (alloc_begin_ptr)
0x1796340 : 0x0 (one_pass_content_end_ptr)
0x1796348 : 0x0   (one_pass_alloc_end_ptr)

0x1796350 : 0x0          (alloc_begin_ptr)
0x1796358 : 0x0 (one_pass_content_end_ptr)
0x1796360 : 0x0   (one_pass_alloc_end_ptr)

0x1796368 : 0x0          (alloc_begin_ptr)
0x1796370 : 0x0 (one_pass_content_end_ptr)
0x1796378 : 0x0   (one_pass_alloc_end_ptr)

So I tried moving it to stack. It seems to work fine.

    struct V : vector<V> {};
    auto v = V{};
    v.emplace_back();
    v.emplace_back();
    v.emplace_back();
    v.emplace_back();

    auto front = v.front();

    v.swap(v.front());
    
    auto v2 = std::move(front);

There are no objects that own itself.


0x7ffc44d02b20 : 0x927320          (alloc_begin_ptr) // v2 on stack
0x7ffc44d02b28 : 0x927380 (one_pass_content_end_ptr)
0x7ffc44d02b30 : 0x927380   (one_pass_alloc_end_ptr)

0x927320 : 0x0          (alloc_begin_ptr)
0x927328 : 0x0 (one_pass_content_end_ptr)
0x927330 : 0x0   (one_pass_alloc_end_ptr)

0x927338 : 0x0          (alloc_begin_ptr)
0x927340 : 0x0 (one_pass_content_end_ptr)
0x927348 : 0x0   (one_pass_alloc_end_ptr)

0x927350 : 0x0          (alloc_begin_ptr)
0x927358 : 0x0 (one_pass_content_end_ptr)
0x927360 : 0x0   (one_pass_alloc_end_ptr)

0x927368 : 0x0          (alloc_begin_ptr)
0x927370 : 0x0 (one_pass_content_end_ptr)
0x927378 : 0x0   (one_pass_alloc_end_ptr)

Why does allocator_traits::destroy() on a vector that owns itself trigger SIGSEGV ?

    //  atraits::destroy(a, &front);   // error SIGSEGV

0x1796320 : 0x1796320          (alloc_begin_ptr)  // It owns itself!!!
0x1796328 : 0x1796380 (one_pass_content_end_ptr)
0x1796330 : 0x1796380   (one_pass_alloc_end_ptr)

[LIVE]

1

There are 1 best solutions below

2
Artyer On

(I'm assuming this line:

auto front = v.front();

is a typo, because v.front() is a default constructed V object that you copy from. This means that that line is essentially auto front = V{}. You meant auto& front = v.front())

&front + 1, &front + 2 and &front + 3 are pointers to empty vectors, these can be destroyed just fine.

If you attempt to destroy what &front points to, this is the vector with 4 elements. The last three elements are those empty vectors, which have no problem being destroyed.
But the first element of front is front itself. This is undefined behaviour because you would call the destructor on an object already being destroyed, but in practice it leads to an infinite loop because it will just call the destructor again and recursively try to destroy the same vector (and a stack overflow which leads to a segfault).

If you simply deallocate it, no destructors are called so there is no infinite loop. You might leak memory if the vector had held other vectors that allocated memory, since the destructors for those vectors wouldn't be called either.

As you've tried, the way to "fix" this is to break the "ownership" cycle, like by moving it to a new vector (V{} = std::move(front) / V{}.swap(front)).