Crash when destructors for static objects are exec

2019-06-05 02:10发布

问题:

There's a subtle bug that wouldn't manifest predictably in a piece of our software.

It happens when global destructors are executing. Often it's a "double-free" error, but I've seen other things as well: NULL-ptr dereference, ptr dereference to an address where nothing is allocated, misaligned access (because the pointer had a garbage value), issues related to a corrupt stack ... the list goes on.

回答1:

The cause of these mysterious & difficult to reproduce bugs: a subtle violation of the One Definition Rule.

A little background ...

Since I was a little tike this software was being linked using the -zmuldefs linker flag, instructing the linker to ignore situations like the following. It's then forced to pick the 1st definition it encounters (and of course the linker warning was being ignored as well):

$ cat /tmp/file1.cc
int x;
int main( int argc, char *argv[] ) { return x; }

$ cat /tmp/file2.cc
double x = 3.14159265358979;

$ gcc /tmp/file{2,1}.cc -o /tmp/test
/tmp/ccuTgbRy.o:(.bss+0x0): multiple definition of 'x'
/tmp/cchvHEav.o:(.data+0x0): first defined here
/usr/bin/ld: Warning: size of symbol 'x' changed from 8 in /tmp/ccYCIypE.o to 4 in /tmp/ccuTgbRy.o
collect2: error: ld returned 1 exit status

$ gcc /tmp/file{2,1}.cc -Wl,-zmuldefs -o /tmp/test
/usr/bin/ld: Warning: size of symbol 'x' changed from 8 in /tmp/ccWaeBBi.o to 4 in /tmp/ccSc9IiE.o

$ /tmp/test; echo $?
68

How this relates back to the problem

There's four basic situations I've encountered where this problem will show up:

$ cat /tmp/file1.cc
double x;  // (1) If file2.cc is linked first it may end up on
           // a dword boundary causing misaligned accesses
           // when used as a double.

std::string mystring; // (2) If file2.cc is linked first, the actual size
                      // of the object is sizeof(char*) so
                      // std::string::string() will clobber memory
                      // after the pointer.

std::string another; // (3)
                     // file1.cc is compiled with -fPIC & put into a
                     // shared library
                     // file2.cc is NOT compiled with -fPIC & is put
                     // into an executable
                     // 
                     // This will cause a very subtle problem: the two
                     // strings share the same piece of memory, but
                     // the constructor will execute once during the executable's
                     // _init() and once for each shared library with its own
                     // variable "another" when their _init() executes.
                     // The destructor will also execute multiple times

$ cat /tmp/file2.cc
int x;
char *mystring;       // (4) Modifying through this ptr will cause undefined
                      // behavior when the other file's "mystring" is used
std::string another;

The ones that result in a size or alignment change should be reported as linker warnings, so someone might be inclined to just fix the problem by renaming the offending variables (or whatever).

However, there's no way to see that the problem exists for the following situations:

  • The object sizes are the same (x is defined as float/int & sizeof(float) == sizeof(int))
  • The the offending variables (with the same size & type) exist in multiple libraries and/or executables

The only solution that will ensure you've eliminated all of these problems:

  • get rid of -zmuldefs
  • ensure all declarations come from headers / include the header where it's defined