Is it possible to get Clang Static Analyzer to understand reference-counting?
Asked Answered
P

0

6

The situation: My code uses a reference-counting pointer class (similar in spirit to boost::intrusive_ptr) to manage its dynamic allocations and avoid memory leaks.

AFAICT this works fine and does not leak memory; however, when I run Clang Static Analyzer on my code that uses this class, Clang doesn't seem to understand the reference-counting logic and therefore generates warnings about memory-leaks and use-after-free errors.

The toy code below (which is greatly simplified for clarity) demonstrates the problem -- it compiles and runs without leaking memory (as indicated by running allocated-object-count it prints to stdout as it goes), but if I run scan-build on it, I get this output:

$ scan-build g++ testrefcount.cpp -o testrefcount
scan-build: Using '/Users/jaf/checker-279/bin/clang' for static analysis
testrefcount.cpp:43:54: warning: Use of memory after it is freed
   const MyRefCountedObject * GetObject() const {return _obj;}
                                                 ^~~~~~~~~~~
testrefcount.cpp:52:8: warning: Potential memory leak
   }
   ^
2 warnings generated.
scan-build: 2 bugs found.
scan-build: Run 'scan-view /var/folders/q9/9w3p5x650wgfpbcws3zhsc6h0000gn/T/scan-build-2019-01-12-122209-5219-1' to examine bug reports.

My question is, is there any way to modify my code such that Clang's Static Analyzer won't emit false-positives here? I suppose I can just wrap the refence-counting methods in #ifndef __clang_analyzer__, but that seems like an ugly solution (analogous to putting tape over the "check engine" light) which would prevent CSA from detecting any actual problems with this code, so I'd prefer to avoid that if there is a better approach. (Yes, I know about shared_ptr and intrusive_ptr and so on -- I wonder how they deal with this issue?)

Self-contained example code follows:

#include <stdio.h>

// This value will keep track of the number of MyRefCountedObject objects alive in
// the process -- just to verify that this program doesn't actually have a memory leak
static int _objectCounter = 0;

// Objects of this class will be reference-counted by the MyRef class
class MyRefCountedObject
{
public:
   MyRefCountedObject(int value) : _value(value), _refCount(0)
   {  
      printf("MyRefCountedObject %p with value %i created.  Current number of MyRefCountedObjects is now %i\n", this, _value, ++_objectCounter);
   }

   ~MyRefCountedObject()
   {  
      printf("MyRefCountedObject %p with value %i destroyed.  Current number of MyRefCountedObjects is now %i\n", this, _value, --_objectCounter);
   }

   int GetValue() const {return _value;}

   inline void IncrementRefCount() const {_refCount++;}
   inline bool DecrementRefCount() const {return (--_refCount == 0);}

private:
   inline MyRefCountedObject &operator=(const MyRefCountedObject &); // deliberately unimplemented

   const int _value;       // An arbitrary payload/data value
   mutable int _refCount;  // how my MyRef's are currently pointing at this object
};

// Represents one reference to a MyRefCountedObject (like a toy shared_ptr)
class MyRef
{
public:
   MyRef(const MyRefCountedObject * item = NULL) : _obj(item) {RefObject();}
   MyRef(const MyRef & rhs) : _obj(NULL) {*this = rhs;}
   ~MyRef() {UnrefObject();}

   inline MyRef &operator=(const MyRef & rhs) {this->SetRef(rhs._obj); return *this;}

   const MyRefCountedObject * GetObject() const {return _obj;}

private:
   void RefObject() {if (_obj) _obj->IncrementRefCount();}

   void UnrefObject()
   {
      if ((_obj)&&(_obj->DecrementRefCount())) delete _obj;
      _obj = NULL;
   }

   void SetRef(const MyRefCountedObject * newObj)
   {
      if (newObj != _obj)
      {
         UnrefObject();
         _obj = newObj;
         RefObject();
      }
   }

   const MyRefCountedObject * _obj;
};

int main(void)
{
   MyRef ref;
   {
      MyRef ref2(new MyRefCountedObject(555));
      ref = ref2;
   }

   printf("At end of main, (ref)'s value is: %i\n", ref.GetObject()->GetValue());
   return 0;
}
Permalloy answered 12/1, 2019 at 20:26 Comment(6)
I was unable to get the Clang Static Analyzer to not generate the 2 issues, potential leak, and potential use-after-free.Forehand
Eljay are you saying that you were able to reproduce the fault but not able to find a fix/workaround for the fault, or are you saying that you were unable to reproduce the fault?Permalloy
I ran CSA and got the 2 faults. I tried refactoring the code, but I was unable rework the code in such a way to remove either fault. CSA is unaware that the code path it has identified is carefully constructed not to have the problems it is identifying. (Multithreading case excluded.)Forehand
I browsed the CSA documentation, and for a reference counted object there are CSA attributes to mark the routines so CSA can proper analyze the code and not emit issues. There does not appear to be a way to avoid using the attributes while also CSA not emitting the issues. I'm not sure the best link for the most up to date CSA documentation, the information I found was a bit old.Forehand
AFAICT the annotations for reference-counting are all specific to Objective C, though. If you've found some that would be appropriate for C++, I'd be interested in reading about them.Permalloy
I made a variant that used std::shared_ptr and std::enable_shared_from_this, and (as you probably had already discovered / known) did not get a CSA issue with those. I don't see what those C++ constructs did different to avoid getting flagged. I didn't see any attribute tags. So I presume CSA itself only considers the file and not the headers or perhaps not any of the system headers (free pass...?). Harumph.Forehand

© 2022 - 2024 — McMap. All rights reserved.