Sanitizers: your new secret weapon to kill bugs

C++ will give you a job for life:

  • Buffer overflows
  • Data races
  • Use after delete
  • Invalid Pointer References
  • Undefined Behaviors
  • Memory Leaks
  • Use of uninitialized memory

What scares me...

  • Hard to reproduce, sporadic bugs
  • Bugs that disappear when you compile in Debug mode
  • Silent data corruption that doesn't trigger a SEGFAULT.

Welcome sanitizers

  • AddressSanitizer: detects use-after-free and buffer overflows

 

  • ThreadSanitizer: detects data races and deadlocks

 

  • MemorySanitizer: detects uninitialized memory reads

 

  • UndefinedBehaviorSanitizer: detects “simple” undefined behaviors

Sanitizers, AKA dynamic code analysis

  • You must run your code (it is not static code analysis)
  • Aims to zero false positives
  • Usually 10X faster than Valgrind
  • Very detailed error report, more convenient than GDB
  • Start using them in 1 minute...

AddressSanitizer: buffer-overflow

    int global_array[100];
    
    int main(int argc, char **argv) 
    {
        int stack_array[100];

        int index = 101 + argc; //index not know at compilation time

        global_array[index] = 42;
        stack_array[index] = 42;

        std::cout <<  global_array[index] << std::endl;
        std::cout <<  stack_array[index] << std::endl;
  
        return 0;
    }

AddressSanitizer: heap-overflow and use after free


    int main(int argc, char **argv)
    {
        int *array = new int[100];
    
        int index = 100 + argc; //argc not know at compilation time
    
        array[index] = 42; // BOOM?
        std::cout <<  array[index] << std::endl;
    
        delete [] array;
    
        array[0] = 23; // BOOM?
        std::cout <<  array[0] << std::endl;
    
        return 0;
    }

AddressSanitizer: container-overflow


    int main(int argc, char **argv)
    {
        std::vector<int> V(8, 42);
        V.resize(5); // capacity still 8
    
        // The memory is still there, but we are not supposed to
        // access this element
        std::cout <<  V[6] << std::endl;
    
        return 0;
    }

Note: previous examples may crash, but not this one.

AddressSanitizer: dangling reference

    const std::vector<int>& Vector42( int size )
    {
         std::vector<int> V(size, 42);
         return V;
    }
    
    int main()
    {
        const std::vector<int>& V = Vector42(8);
    
        // No problem, right?
        std::cout <<  V[6] << std::endl;
        // BOOOOM
        return 0;
    }

AddressSanitizer: memory leaks


    int main()
    {
        int *g = new int;
        g = nullptr; // Lost the pointer.

        return 0;
    }

Recently I found a bug like this in Qt, where you are not supposed to call delete explicitly...

AddressSanitizer overhead

  • CPU: 2X slowdown
  • RAM: 1.5X-3X memory overhead

ThreadSanitizer: example

    struct A 
    {
        virtual ~A() 
        {
            F();
        }
        virtual void F() 
        {
            printf("In A");
        }
    };

    struct B : public A 
    {
        virtual void F() 
        {
            printf("In B");
        }
    };
    // guess what it prints...

I recently fixed a bug like this one... if you don't see it, it is normal

    class A {
      public:
        A() : done_(false) {}
        virtual void F() { printf("A::F\n"); }
        void Done() {
                std::unique_lock<std::mutex> lk(m_);
                done_ = true;
                cv_.notify_one();
        }
        virtual ~A() {
            std::unique_lock<std::mutex> lk(m_);
            cv_.wait(lk, [this] {return done_;});
        }
     private:
        std::mutex m_;
        std::condition_variable cv_;
        bool done_;
    };
    
    class B : public A {
      public:
        virtual void F() { printf("B::F\n"); }
        virtual ~B() {}
    };
    
    int main() {
        A *a = new B;
        std::thread t1([a] {a->F(); a->Done();});
        std::thread t2([a] {delete a;});
        t1.join(); t2.join();
    } 

ThreadSanitizer overhead

  • CPU: 4X-10X slowdown
  • RAM: 5X-8X memory overhead

 If you write multi-threaded code, it does have bugs. Deal with it and sanitize it.

UndefinedBehaviorSanitizer

    int main(int argc, char **argv)
    {
        int t = 1 << 16;
        std::cout << t * t << std::endl;
        return 0;
    }

Bugs makes you sad? Start being awesome with Sanitizers!

Sanitizers

By Davide Faconti