C++ – Why RTLD_DEEPBIND and RTLD_LOCAL do not prevent conflicts of static class member symbols

Why RTLD_DEEPBIND and RTLD_LOCAL do not prevent conflicts of static class member symbols… here is a solution to the problem.

Why RTLD_DEEPBIND and RTLD_LOCAL do not prevent conflicts of static class member symbols

I’m trying to write a simple plugin system for my application and want to prevent plugins from stepping on symbols for each other, but RTLD_DEEPBIND and RTLD_LOCAL don’t seem to be enough to solve the problem of static class members having the same name in different plugins.

I

wrote a concise example to illustrate what I mean.
I compiled and ran it like this :

g++ -c dumb-plugin.cpp -std=c++17 -fPIC
gcc -shared dumb-plugin.o -o dumb1.plugin
cp dumb1.plugin dumb2.plugin
g++ main.cpp -ldl -o main
./main

The contents of the output file for the second plugin indicate that it reuses the classes of the first plugin.

How can I avoid this?

EDIT: I compiled the plugin with clang (not just plugins) and even though all the RTLD_DEEPBIND stuff is in main.cpp, it still works, it still compiles in g++. When compiling the plugin with gcc 10.3 or 11.1, it doesn’t work even though I tried -bsymbolic. Is this a bug?

If I run readelf on DSO compiled/linked with clang, I see these two lines:

21: 00000000000040b0     4 OBJECT  UNIQUE DEFAULT   26 _ZN9DumbClass7co[...] _ZN9DumbClass7co[...]
25: 00000000000040b0     4 OBJECT  UNIQUE DEFAULT   26 _ZN9DumbClass7co[...]

With gcc I get:

20: 00000000000040a8     4 OBJECT  WEAK   DEFAULT   24 _ZN9DumbClass7co[...]
27: 00000000000040a8     4 OBJECT  WEAK   DEFAULT   24 _ZN9DumbClass7co[...]

Use WEAK instead of UNIQUE under the BIND column.

dumb-plugin.cpp:

#include <dlfcn.h>
#include <cstdio>
#include <string>

int global_counter = 0;
static int static_global_counter = 0;

std::string replace_slashes(const char * str) {
    std::string s;
    for (const char* c = str; *c != '\0'; c++)
        s += (*c == '/')?
            '#' : *c;
    return s;
}

void foo() {}

class DumbClass {
    public:
        static inline int counter = 0;
};

extern "C" void plugin_func() {
    static int static_local_counter = 0;

Dl_info info;
    dladdr((void*)foo, &info);

std::string path = "plugin_func() from: " + replace_slashes(info.dli_fname);
    auto fp = std::fopen(path.c_str(), "w");

fprintf(fp, "static local counter: %d\n",  static_local_counter++);
    fprintf(fp, "DumbClass::counter: %d\n",    DumbClass::counter++);
    fprintf(fp, "global counter: %d\n",        global_counter++);
    fprintf(fp, "static global counter: %d\n", static_global_counter++);

std::fclose(fp);
}

main.cpp :

#include <dlfcn.h>
#include <iostream>
#include <unistd.h>
#include <string.h>

int main () {
    char path1[512], path2[512];
    getcwd(path1, 512);
    strcat(path1, "/dumb1.plugin");
    getcwd(path2, 512);
    strcat(path2, "/dumb2.plugin");

auto h1 = dlopen(path1, RTLD_NOW | RTLD_LOCAL | RTLD_DEEPBIND);
    auto h2 = dlopen(path2, RTLD_NOW | RTLD_LOCAL | RTLD_DEEPBIND);

auto func = (void(*)())  dlsym(h1, "plugin_func");
    func();
    func =      (void(*)())  dlsym(h2, "plugin_func");
    func();
}

Solution

gcc treats static inline data members (and static data members of class templates, inline or not, and static variables in inline functions, and perhaps other things) as globally unique symbols (GNU extensions in ELF format). By design, each process has only one such symbol with a given name.

Clang implements things like plain weak symbols. When you use RTLD_LOCAL and RTLD_DEEPBIND, they do not conflict.

There are several ways to avoid conflicts, but all require action from the plugin writer. The best way to IMO is to use hidden symbol visibility by default, only open symbols that mean dlsymd.

Related Problems and Solutions