views:

108

answers:

3

Please note that I don't want to solve any problem with my question - I was thinking about probabilities of things to happen and thus was wondering about something:

What exactly happens if you delete on object and use gcc as compiler?

Last week I was investigating a crash, where a race condition lead to an double delete of an object.

The crash occured when calling the virtual destructor of the object, because the pointer to the virtual function table already was overwritten.

Is the virtual function pointer overwritten by the first delete?

If not, is the second delete safe then, as long as no new memory allocation is made in the meantime?

I'm wondering why the problem I had was not recognized before and the only exlanation is that either the virtual function table is overwritten immediatly during the first delete or the second delete does not crash.

(The first means that the crash always occurs on the same location if the "race" happens - the second one, that usually nothing happens when the race happens - and only if a third thread overwrites the delete object in the meantime the problem occurs.)


Edit/Update:

I did a test, the following code crashes with a segfault (gcc 4.4, i686 and amd64):

class M
{
private:
  int* ptr;
public:
  M() {
  ptr = new int[1];
  }
  virtual ~M() {delete ptr;}
};

int main(int argc, char** argv)
{
  M* ptr = new M();
  delete ptr;
  delete ptr;
}

If I remove the 'virtual' from the dtor, the program is aborted by glibc because it detects the double-free. With the 'virtual' the crash occurs when doing the indirect function call to the destructor, because the pointer to the virtual function table is invalid.

On both amd64 and i686 the pointer points to a valid memory region (heap), but the value there is invalid (a counter? It's very low, e.g 0x11, or 0x21) so the 'call' (or 'jmp' when the compiler did a return-optimization) jumps to an invalid region.

Program received signal SIGSEGV,

Segmentation fault. 0x0000000000000021

in ?? () (gdb)

#0 0x0000000000000021 in ?? ()

#1 0x000000000040083e in main ()

So with the above mentioned conditions, the pointer to the virtual function table is ALWAYS overwritten by the first delete, so the next delete will jump to nirvana if the class has a virtual destructor.

+5  A: 

Deleting something twice is undefined behaviour - you don't need any further explanation than that, and it is generally fruitless to look for one. It might cause a program to crash, it might not, but it is always a bad thing to do and the program will always be in an unknown state after you have done it.

anon
Knowledge about what may happen with deleted objects and what not helps in core dump analysis.
IanH
@Ian I prefer not to have core dumps in the first place. Also, you really can't tell what will happen unless you have intimate knowledge of the memory allocation system, which few people do, and which may well change from release to release of the compiler.
anon
I prefer not to have bugs at all, but they occur - and so do crashes.Then it is very useful to know what may happen (and what can be the reason for what happened) and what not.In long running projects you usually don't change the compiler often. And hopefully the compiler and it's libc do not change memory allocator without noticing.
IanH
+5  A: 

It is very dependent on the implementation of the memory allocator itself, not to mention any application dependent failures as overwritting v-table of some object. There are numerous memory allocator schemes all of which differ in capabilities and resistance to double free() but all of these share one common property: your application will crash at some time after the second free().

The reason for the crash is usually that memory allocator dedicates small amount of memory before(header) and after(footer) each allocated chunk of memory to store some implementation specific details. Header usually defines size of the chunk and address of the next chunk. Footer is usually pointer to the header of the chunk. Deleting twice usually at least involves checking if adjacent chunks are free. Thus your program will crash if:

1) pointer to the next chunk has been overwritten and the second free() causes segfault when trying to access the next chunk.

2) the footer of the previous chunk has been modified and access to the header of the previous chunk causes segfault.

If the application survives, it means that free() has either corrupted memory in various locations or will add free chunk which overlaps one of already free chunks, leading to data corruption in the future. Eventually your program will segfault at one of following free() or malloc() involving the corrupted memory areas.

buratinas
What happens to the deleted element, if there's no more malloc after the delete?
IanH
I did some tests with gcc 4.4: It really seems that the first delete overwrites the virtual function table, so the crash occurs when the virtual destructor is called the second time.With a non-virtual destructor, the glibc detects the double-free and aborts the program.
IanH
I added the test I did and its result to my question.
IanH
Well, this means that when marking the chunk free, glibc uses some of the freed memory to implement some kind of data structure (to sort chunks according to their size for example).
buratinas
+1  A: 

By executing delete twice (or even free), the memory may already be reallocated and by executing delete again may cause memory corruption. The size of the block of memory allocated is often held just before the memory block itself.

If you have a derived class, do not call delete on the derived class (child). If it is not declared virtual then only the ~BaseClass() destructor is called leaving any allocated memory from the DerivedClass to persist and leak. This assumes that the DerivedClass has extra memory allocated above and beyond that of the BaseClass which must be freed.

i.e.

BaseClass* obj_ptr = new DerivedClass;  // Allowed due to polymorphism.
...
delete obj_ptr;  // this will call the destructor ~Parent() and NOT ~Child()
0A0D
See my update above: At least with a new gcc a double-delete crashes immediately, even if there is no new/malloc in the meantime.And I know the reason why destructors almos always need to be virtual.
IanH