Flamefire
Flamefire

Reputation: 5807

Use UBSAN with dynamically loaded shared libraries

I tried to use UBSAN in a project and run into an issue which seems impossible to fix: The project uses a plugin system implemented via shared libraries. That is each plugin provides a factory method which returns an instance of some abstract class with a plugin-specific derived class. The project then iterates over all shared libraries in a folder, opens them with dlopen, gets the factory method via dlsym and creates plugin instance which is then used.

However at usage of any interface method UBSAN throws member call on address 0x... which does not point to an object of type '...'

MWE:

foo.h

struct Foo{
  virtual int g() = 0;
};
extern "C" Foo* create();

foo.cpp

#include "foo.h"
struct Bar: Foo{
    int g(){ return 42; }
};
Foo* create(){
  return new Bar();
}

main.cpp

#include "foo.h"
#include <dlfcn.h>
#include <cassert>

int main(){
  void* h = dlopen("libfoo.so", RTLD_GLOBAL | RTLD_NOW);
  assert(h);
  void* c = dlsym(h, "create");
  assert(c);
  using create_t = Foo*();
  Foo* f  = reinterpret_cast<create_t*>(c)();
  return f->g() != 42;
}

Compile with:

https://whatofhow.wordpress.com/2015/03/17/odr-rtti-dso explains, that this is due to the RTTI info in the shared library and the binary being different.

A very similar issue happens, when you export a function in the shared library, import it with dlsym and try to call it. The result will be call to function <...> through pointer to incorrect function type '<...>' with -fsanitize=function for clang.

Is there any way to solve this? I'm not using Clang or playing with -fvisibility so no idea what to do here.

Upvotes: 1

Views: 896

Answers (1)

Flamefire
Flamefire

Reputation: 5807

Using clang with -fsanitize=function does already report a violation for the create call:

call to function create through pointer to incorrect function type 'Foo ()()'

This again looks like a false positive and occurs only if the shared library AND the executable are compiled with '-fsanitize=...`.

Comparing the typeinfo of the differently compile shared library with nm -C libfoo.so | grep typeinfo shows that the one with the sanitizer has an additional typeinfo for typeinfo for Foo* ().

Why typeinfo? Well UBSAN compares the typeinfo to decide whether the passed pointer (function or class) is the expected one. typeinfo comparison is done by pointer comparison. This is great for speed but introduces a subtle issue: Even when the actual types are literally the same, they might not share the same typeinfo.

This is the case here: The typeinfo in the library and executable are not merged, so there are 2 instances of the same typeinfo.

The solution is to pass -rdynamic when creating the executable. From the GCC manual

Pass the flag -export-dynamic to the ELF linker, on targets that support it. This instructs the linker to add all symbols, not only used ones, to the dynamic symbol table. This option is needed for some uses of dlopen or to allow obtaining backtraces from within a program.

It seems that here we have such a "use of dlopen".

For CMake use the property ENABLE_EXPORTS on the executable target.

For some more fun with UBSAN problems see the related question Call to function (unknown) through pointer to incorrect function type

Upvotes: 1

Related Questions