Search code examples
c++c++11segmentation-fault

Segmentation error in destructor if the array is used


I get a segmentation error that I cannot understand when cleaning up the vector. Removing array access (3rd line from the bottom) removes the crash, even though the crash is not on that line.

Code below is the smallest repro I ended with and is extracted from a bigger codebase (I confirmed it crashes in dtor and not on array write).

#include <array>
#include <cstddef>
#include <iostream>
#include <memory>
#include <vector>

class Deletable {
public:
  virtual ~Deletable() = default;
};

template <typename V> class DeletableHandle : public Deletable {
public:
  virtual V *get() = 0;
  virtual const V *get() const = 0;
};

template <typename V> class DeletableHolder final : public DeletableHandle<V> {
public:
  V *get() override { return &value_; }
  const V *get() const override { return &value_; }

private:
  V value_;
};

class InferenceContext2 {
public:
  template <size_t LI> auto *LayerScratchSpace() {
    using Area = std::array<int, 4>;
    while (buffers_.size() <= LI) {
      buffers_.emplace_back(nullptr);
    }
    if (buffers_[LI] == nullptr) {
      buffers_[LI] = std::make_unique<DeletableHolder<Area>>();
    }
    return reinterpret_cast<Area *>(buffers_[LI].get());
  }

private:
  std::vector<std::unique_ptr<Deletable>> buffers_;
};

int main() {
  InferenceContext2 ctx;
  auto area = ctx.LayerScratchSpace<0>();
  std::cout << area->size() << " " << (reinterpret_cast<intptr_t>(&area) % 128)
            << " " << alignof(std::remove_pointer_t<decltype(area)>) << "\n";

  // Commenting out the line below removes the crash
  (*area)[0] = 1;
  return 0;
}

On my bigger project I have ASAN and this is what it is saying (stack trace is different):

AddressSanitizer:DEADLYSIGNAL
=================================================================
==12==ERROR: AddressSanitizer: SEGV on unknown address 0x55f800000009 (pc 0x55f85eadc1c1 bp 0x7ffd295108b0 sp 0x7ffd29510890 T0)
==12==The signal is caused by a READ memory access.
    #0 0x55f85eadc1c1 in std::default_delete<uchen::memory::Deletable>::operator()(uchen::memory::Deletable*) const /usr/lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/unique_ptr.h:85:2
    #1 0x55f85eadc0fb in std::unique_ptr<uchen::memory::Deletable, std::default_delete<uchen::memory::Deletable>>::~unique_ptr() /usr/lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/unique_ptr.h:361:4
    #2 0x55f85eadc064 in void std::destroy_at<std::unique_ptr<uchen::memory::Deletable, std::default_delete<uchen::memory::Deletable>>>(std::unique_ptr<uchen::memory::Deletable, std::default_delete<uchen::memory::Deletable>>*) /usr/lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/stl_construct.h:88:15
    #3 0x55f85eadd214 in void std::_Destroy<std::unique_ptr<uchen::memory::Deletable, std::default_delete<uchen::memory::Deletable>>>(std::unique_ptr<uchen::memory::Deletable, std::default_delete<uchen::memory::Deletable>>*) /usr/lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/stl_construct.h:149:7
    #4 0x55f85eadd1e6 in void std::_Destroy_aux<false>::__destroy<std::unique_ptr<uchen::memory::Deletable, std::default_delete<uchen::memory::Deletable>>*>(std::unique_ptr<uchen::memory::Deletable, std::default_delete<uchen::memory::Deletable>>*, std::unique_ptr<uchen::memory::Deletable, std::default_delete<uchen::memory::Deletable>>*) /usr/lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/stl_construct.h:163:6
    #5 0x55f85eadd1ac in void std::_Destroy<std::unique_ptr<uchen::memory::Deletable, std::default_delete<uchen::memory::Deletable>>*>(std::unique_ptr<uchen::memory::Deletable, std::default_delete<uchen::memory::Deletable>>*, std::unique_ptr<uchen::memory::Deletable, std::default_delete<uchen::memory::Deletable>>*) /usr/lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/stl_construct.h:195:7
    #6 0x55f85eadd0d0 in void std::_Destroy<std::unique_ptr<uchen::memory::Deletable, std::default_delete<uchen::memory::Deletable>>*, std::unique_ptr<uchen::memory::Deletable, std::default_delete<uchen::memory::Deletable>>>(std::unique_ptr<uchen::memory::Deletable, std::default_delete<uchen::memory::Deletable>>*, std::unique_ptr<uchen::memory::Deletable, std::default_delete<uchen::memory::Deletable>>*, std::allocator<std::unique_ptr<uchen::memory::Deletable, std::default_delete<uchen::memory::Deletable>>>&) /usr/lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/alloc_traits.h:848:7
    #7 0x55f85eadd09e in std::vector<std::unique_ptr<uchen::memory::Deletable, std::default_delete<uchen::memory::Deletable>>, std::allocator<std::unique_ptr<uchen::memory::Deletable, std::default_delete<uchen::memory::Deletable>>>>::~vector() /usr/lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/stl_vector.h:680:2
    #8 0x55f85ead8d24 in uchen::(anonymous namespace)::InferenceContext2::~InferenceContext2() /proc/self/cwd/test/arena.test.cc:71:7

Reproduces with GCC and Clang. Build with --std=c++20. Compiler versions:

eugene-aurora:~/code$ g++ --version
g++ (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0
Copyright (C) 2021 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

eugene-aurora:~/code$ clang++ --version
Ubuntu clang version 15.0.7
Target: x86_64-pc-linux-gnu
Thread model: posix
InstalledDir: /usr/bin

Solution

  • buffers_[LI].get() returns the address of a DeletableHolder.
    static_cast<DeletableHolder*>(buffers_[LI].get())->get() invokes get() on that DeletableHolder, and that is indeed an Area *.

    By treating the first few bytes of your DeletableHolder as an Area and writing to the first element of that "array", you blew away the vtable that holds the address of the virtual destructor.

    Think twice before silencing compiler warnings with C-style casts or reinterpret_cast. static_cast would have warned you about this.