c++undefined-behaviorubsan

C++ UBSAN produces false positives with derived objects


I wanted to use UBSAN (undefined behavior sanitizer) but found it completely worthless as it reports to many false positives.

E.g. a simple std::make_shared<int>(42); is enough to trigger warnings like

member access within address 0x00000236de70 which does not point to an object of type '_Sp_counted_base'

Reducing this example to a MWE shows that the problem is more general with base classes and inheritance:

Example:

struct Foo{
    int f(){ return g(); }
    virtual int g() = 0;
};

struct Bar: Foo{
    int g(){ return 42; }
};

int main(){
    auto f = new Bar();
    return f->g();
}

Compile with -fsanitize=undefined and watch

example.cpp:15:16: runtime error: member call on address 0x000000726e70 which does not point to an object of type 'Bar'

0x000000726e70: note: object has invalid vptr

See https://godbolt.org/z/0UiVtu.

How are not even these simple cases properly handled? Did I miss anything? How should I properly use UBSAN to check my code? (This requires [almost] no false positives)

Edit: As it seems the MWE only works on godbolt, the original code looks like this:

#include <boost/iostreams/device/mapped_file.hpp>
#include <boost/iostreams/stream.hpp>
using MMStream = boost::iostreams::stream<boost::iostreams::mapped_file_source>;

int main(){
  MMStream stream;
  stream.open("a.out");
  return !stream;
}

Compile with clang++-8 -fsanitize=undefined -fvisibility=hidden -I /opt/boost_1_64_0/include/ test.cpp /opt/boost_1_64_0/lib/libboost_iostreams.so and run which results in errors like

runtime error: member call on address 0x00000126ef30 which does not point to an object of type 'boost::detail::sp_counted_base'


Solution

  • Trying to answer this myself after the comments and creating another MWE.

    TLDR: Make sure all classes containing virtual functions are exported when compiling with -fvisibility=hidden

    Consider a shared library Foo with

    foo.h

    #define EXPORT __attribute__((visibility("default")))
    
    struct Foo{
        virtual int g() = 0;
    };
    
    struct Bar: Foo{
        int g(){ return 42; }
    };
    
    EXPORT Foo* create();
    

    foo.cpp #include "foo.h"

    Foo* create(){
      return new Bar();
    }
    

    Compiled with clang++-8 foo.cpp -shared -fPIC -o foo.so

    And an executable linked against this using the virtual functions but with -fvisibility:

    main.cpp:

    #include "foo.h"
    
    int main(){
      Foo* f  = create();
      return f->g() != 42;
    }
    

    Compiled with clang++-8 -fsanitize=undefined -fvisibility=hidden main.cpp foo.so

    This will report

    runtime error: member call on address 0x00000290cea0 which does not point to an object of type 'Foo'

    This is the same error as described in https://bugs.llvm.org/show_bug.cgi?id=39191 (thanks @Nikita Petrenko)

    Summary: With fvisibility=hidden not exported symbols (functions, classes not decorated with the attribute __attribute__((visibility("default"))) are not considered the same when used in different DSOs (e.g. executable and shared library). Hence the base class Foo in the shared library and the executable are distinct (they have different vtables) which UBSAN detects: The executable "expects" an object witg vtable of Exe::Foo but instead gets Library::Foo

    In the boost case the class sp_counted_base is the culprit as it is not exported until Boost 1.69 which adds BOOST_SYMBOL_EXPORT, so switching to Boost 1.69+ fixes the issue.