0

I am running into a weird issue at work where after updating from RHEL 7 (linux kernel 3.10.0, GCC 4.8.5) to RHEL 8 (linux kernel 4.18.0, GCC 8.3.1), our enums have started to cause problems while destructing. From my best diagnosis in gdb, it is trying to call the destructor on the same static object more than once (once for each lib that instantiates the enums and is used to build the executable in question) and segfaulting on the second attempt, as the object has already been destroyed.

Here is the backtrace:

#0  0x0000000000000000 in ?? ()
#1  0x00007ffff3c91b6f in __tcf_2 () at /sourcepath/ExampleEnum.H:106
#2  0x00007ffff68ae3c7 in __cxa_finalize () from /lib64/libc.so.6
#3  0x00007ffff3c33c87 in __do_global_dtors_aux () from /libpath/lib64/libsecond_lib.so
#4  0x00007fffffff9c10 in ?? ()
#5  0x00007ffff7de42a6 in _dl_fini () from /lib64/ld-linux-x86-64.so.2

This is the second time it reaches that line of ExampleEnum.H in __tcf_2, a function related to static destruction. The first time is no problem.

Here is the structure of the enums:

#ifndef _EXAMPLEENUM_H
#define _EXAMPLEENUM_H

#include "OurString.H"

#define EXAMPLEENUM_SOURCE_LIST(enum) \
    enum(THIS_EXAMPLE_ENUM, "THIS_EXAMPLE", "", false),\
    enum(ExampleEnumMax, "ExampleEnumMax", "error", false)

#define NAME_GENERATOR(name, guiname, description, p4) name
#define GUI_NAME_STR_GENERATOR(name, guiname, description, p4) guiname

class Example {
    public:
    enum Enum {
       EXAMPLEENUM_SOURCE_LIST(NAME_GENERATOR)
    };
    static const int NUM_FIELDS = ExampleEnumMax + 1;
    static const char* names[NUM_FIELDS];
};
typedef Example::Enum ExampleEnum

extern const OurString ExampleEnum_GuiName[Example::ExampleEnumMax + 1];
#ifdef CONSTRUCT_ENUM_STRINGS
const OurString ExampleEnum_GuiName[Example::ExampleEnumMax + 1] = {
    EXAMPLEENUM_SOURCE_LIST(GUI_NAME_STR_GENERATOR)
};
#endif

#endif

And then in the libs where it is used, this names.C is compiled into the lib:

#define CONSTRUCT_ENUM_STRINGS 1
#include <enumpath/ExampleEnum.H>
#undef CONSTRUCT_ENUM_STRINGS

const char* Example::names[Example::NUM_FIELDS] = {
    EXAMPLEENUM_SOURCE_LIST(GUI_NAME_STR_GENERATOR)
};

We have a band-aid solution that basically just covers up the problem, ie calling _exit(0) at the end of main() skips all destructors, including static destructors which pose the problem so it doesn't segfault. However, obviously we want to fix the way our enums are handled such that we can run all necessary destructors (and no more than necessary) without segfaulting.

Is there anything obviously wrong with our enums? They have been working through several kernel/gcc versions and have only recently posed a problem.

Is there likely to be anything wrong with how they are used in the libs? This problem only occurs when an executable is compiled with multiple libs that use the same enum, which is unfortunately quite often. Is there some strict tree of import dependency structure we could keep to to fix this?

Why did it work up until we updated the OS?

EDIT: Concerns about OurString's destructor have been raised, I didn't include it because it was trivial: ~OurString() throw () {}

ALSO: a little more debugging and going through a version compiled by GCC 4.8.5 that doesn't segfault shows me that __tcf_2 is entered twice there too, so my theory about improperly calling the destructor multiple times is wrong, and it looks like @PaulMcKenzie's theory of static initialization order is likely.

Thanks in advance!

  • 1
    If these are global objects, and they are defined in different translation units, and the usage of one or more is occurring on destruction, the order of destruction is implementation-defined, or not concretely specified. Basically the [static initialization order fiasco issue](https://stackoverflow.com/questions/29822181/prevent-static-initialization-order-fiasco-c), but occurring on destruction. – PaulMcKenzie Apr 02 '21 at 17:07
  • 1
    What is `OurString`? I think there are details you're leaving out, such as whether that type has a user-defined destructor that manages, either implicitly or explicitly, dynamically allocated memory. -- *Why did it work up until we updated the OS?* -- Maybe you were lucky (or unlucky) that the symptoms of undefined behavior was that your program "worked". – PaulMcKenzie Apr 02 '21 at 17:17
  • Yeah I should have mentioned that, but OurString is a trivial destructor, so I didn't think it was the problem. I thought it was the existence of it that was the problem (imo it tries to call the destructor but since it has already been destroyed, the function address is 0 and that's how we end up with 0 in the $pc.) ~OurString() throw () {}. Thank you for the suggestion of the static initialization order fiasco issue, I had seen it as a possibility but wasn't sure if it applied to destruction as well. – Eamon Collins Apr 02 '21 at 18:01
  • Please provide a [mre] – Alan Birtles Apr 02 '21 at 18:13
  • 1
    Let's see `OurString`. What do you get for this: `std::cout << std::is_trivially_destructible();`? The `std::is_trivially_destructible` is in ``. An empty destructor does not mean it is trivially destructible -- as a matter of fact, you have a user-defined destructor, even though it's empty, thus it is *not* trivially destructible. – PaulMcKenzie Apr 02 '21 at 18:17
  • [See this](http://coliru.stacked-crooked.com/a/0bc39c81cb289219). Trivially destructible means no user-defined destructor whatsoever, not even an empty one. – PaulMcKenzie Apr 02 '21 at 18:22
  • [and this](http://coliru.stacked-crooked.com/a/ceb17c50515bfa57) – PaulMcKenzie Apr 02 '21 at 18:28
  • You could dynamically allocate all of your global objects, and then do not `delete` them. That will definitely get rid of the destructor issue, since it would never be called. The downside is that you have a memory leak, at least it would be reported by tools like valgrind, on program closure. If you can live with that annoyance, then that may work. – PaulMcKenzie Apr 02 '21 at 18:31
  • @PaulMcKenzie I see. That is interesting, but as long as it doesn't modify dynamically allocated memory, would it be a concern for a segfault? Especially since it (I think) manages to run once before the segfault? That is an interesting idea about dynamically allocating them. It would be a "memory leak", but would in practice take up the same amount of memory for almost exactly as long as they are currently, as they currently exist for the whole lifetime of the program anyway. – Eamon Collins Apr 02 '21 at 18:33
  • I would just try the dynamic allocation and see what happens. If the program didn't segfault before exiting `main` (or whatever the main entry point is for your app), then I don't believe it will segfault with the dynamic allocation (but again, you will have a memory leak, which should be harmless, but annoying). Please follow up as an answer if that works for you. – PaulMcKenzie Apr 02 '21 at 18:38

0 Answers0