llvm / llvm-project

The LLVM Project is a collection of modular and reusable compiler and toolchain technologies.
http://llvm.org
Other
26.76k stars 10.96k forks source link

Constructor without visible definition breaks the static analyzer's reasoning about unrelated fields #55194

Open josephcsible opened 2 years ago

josephcsible commented 2 years ago

Consider this C++ code:

struct SomeStruct {
  SomeStruct() noexcept;
};

//SomeStruct::SomeStruct() noexcept {}

class SomeClass {
  const bool b;
  const SomeStruct s;

public:
  SomeClass() : b(true) {}
  operator bool() const { return b; }
};

void f() {
  int *p = new int;
  if (SomeClass())
    delete p;
}

When I run clang --analyze -Xanalyzer -analyzer-output=text on it, I get this:

q72007867.cpp:20:1: warning: Potential leak of memory pointed to by 'p' [cplusplus.NewDeleteLeaks]
}
^
q72007867.cpp:17:12: note: Memory is allocated
  int *p = new int;
           ^~~~~~~
q72007867.cpp:18:7: note: Assuming the condition is false
  if (SomeClass())
      ^~~~~~~~~~~
q72007867.cpp:18:3: note: Taking false branch
  if (SomeClass())
  ^
q72007867.cpp:20:1: note: Potential leak of memory pointed to by 'p'
}
^
1 warning generated.

Uncommenting the definition of SomeStruct's constructor makes the warning go away, though. Swapping the order of const bool b; and const SomeStruct s; also makes it go away.

llvmbot commented 2 years ago

@llvm/issue-subscribers-clang-static-analyzer

steakhal commented 2 years ago

It seems like the first exploded node/state from which the two cases differ is State 988, which is tagged by ExprEngine : Prepare for object construction.

image We can see that if we cannot see the sub-object constructors, e.g. we don't see the ctor of SomeStruct, we do an invalidation. I guess, the s member of a SomeClass might be able to reinterpret-cast its this pointer to something else, do some pointer arithmetic, and modify the const b member in some way. However, I would rather say that we shouldn't invalidate the neighbor members in such cases. Or at least, we should respect const members to preserve their values.

Consequently, at the operator bool() call, we will do a state-split, and construct a path on which we don't delete p; causing this FP.

In contrast to this, when we have the body of the SomeStruct ctor, we won't invalidate, thus we can accurately track that operator boo() should result in true; releasing p.

@haoNoQ WDYT?

These dumps had to be suffixed with the txt extension to make GitHub happy. without.html.txt with.html.txt

josephcsible commented 2 years ago

I guess, the s member of a SomeClass might be able to reinterpret-cast its this pointer to something else, do some pointer arithmetic, and modify the const b member in some way.

Since b is const, modifying it like that would be UB. If we had to assume that code we can't see might invoke UB, then we can't reason about anything, since then we'd also have to assume it could reach up into the stack and change our p to be nullptr before we can release it. So I agree we should assume that doesn't happen.

Two other things worth mentioning: