I am trying to write a simple plugin system for an application and would like to prevent plugins from stomping on each others symbols, however RTLD_DEEPBIND and RTLD_LOCAL don't seem to be enough when it comes to static class members when they happen to have the same name in different plugins.
I wrote a stripped down example to show 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
And the content of the output file for the second plugin showed that it reused the the class from the first plugin.
How can I avoid this?
EDIT: I compiled the plugin with clang(not main just the plugin) and it worked despite all of the RTLD_DEEPBIND stuff being in main.cpp which was still compiled with g++. It didn't work when the plugin was compiled with gcc 10.3 or 11.1 even when I tried -Bsymbolic. Is this a bug?
If I run readelf on the DSO compiled/linked with clang i see these 2 lines:
21: 00000000000040b0 4 OBJECT UNIQUE DEFAULT 26 _ZN9DumbClass7co[...]_ZN9DumbClass7co[...]
25: 00000000000040b0 4 OBJECT UNIQUE DEFAULT 26 _ZN9DumbClass7co[...]
and with gcc i get:
20: 00000000000040a8 4 OBJECT WEAK DEFAULT 24 _ZN9DumbClass7co[...]
27: 00000000000040a8 4 OBJECT WEAK DEFAULT 24 _ZN9DumbClass7co[...]
with 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();
}
gcc
implements static inline data members (and also static data members of class templates, inline or not, and static variables in inline functions, and perhaps other things as well) as global unique symbols (a GNU extension to the ELF format). There is only one such symbol with a given name per process, by design.
clang
implements such things as normal weak symbols. These will not collide when RTLD_LOCAL and RTLD_DEEPBIND are used.
There are several ways to avoid collisions, but all of them require plugin writers to take an action. The best way IMO is to use hidden symbol visibility by default, only opening symbols that are meant to be dlsym
d.