[analyzer] Catch leaking stack addresses via stack variables

Not only global variables can hold references to dead stack variables.
Consider this example:

  void write_stack_address_to(char **q) {
    char local;
    *q = &local;
  }

  void test_stack() {
    char *p;
    write_stack_address_to(&p);
  }

The address of 'local' is assigned to 'p', which becomes a dangling
pointer after 'write_stack_address_to()' returns.

The StackAddrEscapeChecker was looking for bindings in the store which
referred to variables of the popped stack frame, but it only considered
global variables in this regard. This patch relaxes this, catching
stack variable bindings as well.

---

This patch also works for temporary objects like:

  struct Bar {
    const int &ref;
    explicit Bar(int y) : ref(y) {
      // Okay.
    } // End of the constructor call, `ref` is dangling now. Warning!
  };

  void test() {
    Bar{33}; // Temporary object, so the corresponding memregion is
             // *not* a VarRegion.
  }

---

The return value optimization aka. copy-elision might kick in but that
is modeled by passing an imaginary CXXThisRegion which refers to the
parent stack frame which is supposed to be the 'return slot'.
Objects residing in the 'return slot' outlive the scope of the inner
call, thus we should expect no warning about them - except if we
explicitly disable copy-elision.

Reviewed By: NoQ, martong

Differential Revision: https://reviews.llvm.org/D107078
This commit is contained in:
Balazs Benics 2021-08-27 11:31:16 +02:00
parent c22bd391bc
commit 6ad47e1c4f
5 changed files with 173 additions and 20 deletions

View file

@ -11,9 +11,9 @@
// //
//===----------------------------------------------------------------------===// //===----------------------------------------------------------------------===//
#include "clang/StaticAnalyzer/Checkers/BuiltinCheckerRegistration.h"
#include "clang/AST/ExprCXX.h" #include "clang/AST/ExprCXX.h"
#include "clang/Basic/SourceManager.h" #include "clang/Basic/SourceManager.h"
#include "clang/StaticAnalyzer/Checkers/BuiltinCheckerRegistration.h"
#include "clang/StaticAnalyzer/Core/BugReporter/BugType.h" #include "clang/StaticAnalyzer/Core/BugReporter/BugType.h"
#include "clang/StaticAnalyzer/Core/Checker.h" #include "clang/StaticAnalyzer/Core/Checker.h"
#include "clang/StaticAnalyzer/Core/CheckerManager.h" #include "clang/StaticAnalyzer/Core/CheckerManager.h"
@ -303,21 +303,53 @@ void StackAddrEscapeChecker::checkEndFunction(const ReturnStmt *RS,
class CallBack : public StoreManager::BindingsHandler { class CallBack : public StoreManager::BindingsHandler {
private: private:
CheckerContext &Ctx; CheckerContext &Ctx;
const StackFrameContext *CurSFC; const StackFrameContext *PoppedFrame;
/// Look for stack variables referring to popped stack variables.
/// Returns true only if it found some dangling stack variables
/// referred by an other stack variable from different stack frame.
bool checkForDanglingStackVariable(const MemRegion *Referrer,
const MemRegion *Referred) {
const auto *ReferrerMemSpace =
Referrer->getMemorySpace()->getAs<StackSpaceRegion>();
const auto *ReferredMemSpace =
Referred->getMemorySpace()->getAs<StackSpaceRegion>();
if (!ReferrerMemSpace || !ReferredMemSpace)
return false;
const auto *ReferrerFrame = ReferrerMemSpace->getStackFrame();
const auto *ReferredFrame = ReferredMemSpace->getStackFrame();
if (ReferrerMemSpace && ReferredMemSpace) {
if (ReferredFrame == PoppedFrame &&
ReferrerFrame->isParentOf(PoppedFrame)) {
V.emplace_back(Referrer, Referred);
return true;
}
}
return false;
}
public: public:
SmallVector<std::pair<const MemRegion *, const MemRegion *>, 10> V; SmallVector<std::pair<const MemRegion *, const MemRegion *>, 10> V;
CallBack(CheckerContext &CC) : Ctx(CC), CurSFC(CC.getStackFrame()) {} CallBack(CheckerContext &CC) : Ctx(CC), PoppedFrame(CC.getStackFrame()) {}
bool HandleBinding(StoreManager &SMgr, Store S, const MemRegion *Region, bool HandleBinding(StoreManager &SMgr, Store S, const MemRegion *Region,
SVal Val) override { SVal Val) override {
const MemRegion *VR = Val.getAsRegion();
if (!VR)
return true;
if (checkForDanglingStackVariable(Region, VR))
return true;
// Check the globals for the same.
if (!isa<GlobalsSpaceRegion>(Region->getMemorySpace())) if (!isa<GlobalsSpaceRegion>(Region->getMemorySpace()))
return true; return true;
const MemRegion *VR = Val.getAsRegion(); if (VR && VR->hasStackStorage() && !isArcManagedBlock(VR, Ctx) &&
if (VR && isa<StackSpaceRegion>(VR->getMemorySpace()) && !isNotInCurrentFrame(VR, Ctx))
!isArcManagedBlock(VR, Ctx) && !isNotInCurrentFrame(VR, Ctx))
V.emplace_back(Region, VR); V.emplace_back(Region, VR);
return true; return true;
} }
@ -344,19 +376,41 @@ void StackAddrEscapeChecker::checkEndFunction(const ReturnStmt *RS,
"invalid after returning from the function"); "invalid after returning from the function");
for (const auto &P : Cb.V) { for (const auto &P : Cb.V) {
const MemRegion *Referrer = P.first;
const MemRegion *Referred = P.second;
// Generate a report for this bug. // Generate a report for this bug.
const StringRef CommonSuffix =
"upon returning to the caller. This will be a dangling reference";
SmallString<128> Buf; SmallString<128> Buf;
llvm::raw_svector_ostream Out(Buf); llvm::raw_svector_ostream Out(Buf);
SourceRange Range = genName(Out, P.second, Ctx.getASTContext()); const SourceRange Range = genName(Out, Referred, Ctx.getASTContext());
Out << " is still referred to by the ";
if (isa<StaticGlobalSpaceRegion>(P.first->getMemorySpace())) if (isa<CXXTempObjectRegion>(Referrer)) {
Out << "static"; Out << " is still referred to by a temporary object on the stack "
else << CommonSuffix;
Out << "global"; auto Report =
Out << " variable '"; std::make_unique<PathSensitiveBugReport>(*BT_stackleak, Out.str(), N);
const VarRegion *VR = cast<VarRegion>(P.first->getBaseRegion()); Ctx.emitReport(std::move(Report));
Out << *VR->getDecl() return;
<< "' upon returning to the caller. This will be a dangling reference"; }
const StringRef ReferrerMemorySpace = [](const MemSpaceRegion *Space) {
if (isa<StaticGlobalSpaceRegion>(Space))
return "static";
if (isa<GlobalsSpaceRegion>(Space))
return "global";
assert(isa<StackSpaceRegion>(Space));
return "stack";
}(Referrer->getMemorySpace());
// This cast supposed to succeed.
const VarRegion *ReferrerVar = cast<VarRegion>(Referrer->getBaseRegion());
const std::string ReferrerVarName =
ReferrerVar->getDecl()->getDeclName().getAsString();
Out << " is still referred to by the " << ReferrerMemorySpace
<< " variable '" << ReferrerVarName << "' " << CommonSuffix;
auto Report = auto Report =
std::make_unique<PathSensitiveBugReport>(*BT_stackleak, Out.str(), N); std::make_unique<PathSensitiveBugReport>(*BT_stackleak, Out.str(), N);
if (Range.isValid()) if (Range.isValid())

View file

@ -1,7 +1,13 @@
// RUN: %clang_analyze_cc1 -analyzer-checker=core,debug.ExprInspection -std=c++11 -verify -analyzer-config eagerly-assume=false %s // RUN: %clang_analyze_cc1 -analyzer-checker=core,debug.ExprInspection -std=c++11 \
// RUN: %clang_analyze_cc1 -analyzer-checker=core,debug.ExprInspection -std=c++17 -verify -analyzer-config eagerly-assume=false %s // RUN: -analyzer-config eagerly-assume=false -verify %s
// RUN: %clang_analyze_cc1 -analyzer-checker=core,debug.ExprInspection -std=c++11 -analyzer-config elide-constructors=false -DNO_ELIDE_FLAG -verify -analyzer-config eagerly-assume=false %s // RUN: %clang_analyze_cc1 -analyzer-checker=core,debug.ExprInspection -std=c++17 \
// RUN: %clang_analyze_cc1 -analyzer-checker=core,debug.ExprInspection -std=c++17 -analyzer-config elide-constructors=false -DNO_ELIDE_FLAG -verify -analyzer-config eagerly-assume=false %s // RUN: -analyzer-config eagerly-assume=false -verify %s
// RUN: %clang_analyze_cc1 -analyzer-checker=core,debug.ExprInspection -std=c++11 \
// RUN: -analyzer-config elide-constructors=false -DNO_ELIDE_FLAG \
// RUN: -analyzer-config eagerly-assume=false -verify=expected,no-elide %s
// RUN: %clang_analyze_cc1 -analyzer-checker=core,debug.ExprInspection -std=c++17 \
// RUN: -analyzer-config elide-constructors=false \
// RUN: -analyzer-config eagerly-assume=false -verify %s
// Copy elision always occurs in C++17, otherwise it's under // Copy elision always occurs in C++17, otherwise it's under
// an on-by-default flag. // an on-by-default flag.
@ -149,12 +155,21 @@ public:
ClassWithoutDestructor make1(AddressVector<ClassWithoutDestructor> &v) { ClassWithoutDestructor make1(AddressVector<ClassWithoutDestructor> &v) {
return ClassWithoutDestructor(v); return ClassWithoutDestructor(v);
// no-elide-warning@-1 {{Address of stack memory associated with temporary \
object of type 'address_vector_tests::ClassWithoutDestructor' is still \
referred to by the stack variable 'v' upon returning to the caller}}
} }
ClassWithoutDestructor make2(AddressVector<ClassWithoutDestructor> &v) { ClassWithoutDestructor make2(AddressVector<ClassWithoutDestructor> &v) {
return make1(v); return make1(v);
// no-elide-warning@-1 {{Address of stack memory associated with temporary \
object of type 'address_vector_tests::ClassWithoutDestructor' is still \
referred to by the stack variable 'v' upon returning to the caller}}
} }
ClassWithoutDestructor make3(AddressVector<ClassWithoutDestructor> &v) { ClassWithoutDestructor make3(AddressVector<ClassWithoutDestructor> &v) {
return make2(v); return make2(v);
// no-elide-warning@-1 {{Address of stack memory associated with temporary \
object of type 'address_vector_tests::ClassWithoutDestructor' is still \
referred to by the stack variable 'v' upon returning to the caller}}
} }
void testMultipleReturns() { void testMultipleReturns() {
@ -176,6 +191,9 @@ void testMultipleReturns() {
void consume(ClassWithoutDestructor c) { void consume(ClassWithoutDestructor c) {
c.push(); c.push();
// expected-warning@-1 {{Address of stack memory associated with local \
variable 'c' is still referred to by the stack variable 'v' upon returning \
to the caller}}
} }
void testArgumentConstructorWithoutDestructor() { void testArgumentConstructorWithoutDestructor() {
@ -246,6 +264,9 @@ struct TestCtorInitializer {
ClassWithDestructor c; ClassWithDestructor c;
TestCtorInitializer(AddressVector<ClassWithDestructor> &v) TestCtorInitializer(AddressVector<ClassWithDestructor> &v)
: c(ClassWithDestructor(v)) {} : c(ClassWithDestructor(v)) {}
// no-elide-warning@-1 {{Address of stack memory associated with temporary \
object of type 'address_vector_tests::ClassWithDestructor' is still referred \
to by the stack variable 'v' upon returning to the caller}}
}; };
void testCtorInitializer() { void testCtorInitializer() {
@ -279,12 +300,21 @@ void testCtorInitializer() {
ClassWithDestructor make1(AddressVector<ClassWithDestructor> &v) { ClassWithDestructor make1(AddressVector<ClassWithDestructor> &v) {
return ClassWithDestructor(v); return ClassWithDestructor(v);
// no-elide-warning@-1 {{Address of stack memory associated with temporary \
object of type 'address_vector_tests::ClassWithDestructor' is still referred \
to by the stack variable 'v' upon returning to the caller}}
} }
ClassWithDestructor make2(AddressVector<ClassWithDestructor> &v) { ClassWithDestructor make2(AddressVector<ClassWithDestructor> &v) {
return make1(v); return make1(v);
// no-elide-warning@-1 {{Address of stack memory associated with temporary \
object of type 'address_vector_tests::ClassWithDestructor' is still referred \
to by the stack variable 'v' upon returning to the caller}}
} }
ClassWithDestructor make3(AddressVector<ClassWithDestructor> &v) { ClassWithDestructor make3(AddressVector<ClassWithDestructor> &v) {
return make2(v); return make2(v);
// no-elide-warning@-1 {{Address of stack memory associated with temporary \
object of type 'address_vector_tests::ClassWithDestructor' is still referred \
to by the stack variable 'v' upon returning to the caller}}
} }
void testMultipleReturnsWithDestructors() { void testMultipleReturnsWithDestructors() {
@ -328,6 +358,9 @@ void testMultipleReturnsWithDestructors() {
void consume(ClassWithDestructor c) { void consume(ClassWithDestructor c) {
c.push(); c.push();
// expected-warning@-1 {{Address of stack memory associated with local \
variable 'c' is still referred to by the stack variable 'v' upon returning \
to the caller}}
} }
void testArgumentConstructorWithDestructor() { void testArgumentConstructorWithDestructor() {
@ -364,4 +397,24 @@ void testArgumentConstructorWithDestructor() {
#endif #endif
} }
struct Foo {
Foo(Foo **q) {
*q = this;
}
};
Foo make1(Foo **r) {
return Foo(r);
// no-elide-warning@-1 {{Address of stack memory associated with temporary \
object of type 'address_vector_tests::Foo' is still referred to by the stack \
variable 'z' upon returning to the caller}}
}
void test_copy_elision() {
Foo *z;
// If the copy elided, 'z' points to 'tmp', otherwise it's a dangling pointer.
Foo tmp = make1(&z);
(void)tmp;
}
} // namespace address_vector_tests } // namespace address_vector_tests

View file

@ -71,6 +71,9 @@ struct UntypedAllocaTest {
void *allocaPtr; void *allocaPtr;
int dontGetFilteredByNonPedanticMode = 0; int dontGetFilteredByNonPedanticMode = 0;
// expected-warning-re@+3 {{Address of stack memory allocated by call to \
alloca() on line {{[0-9]+}} is still referred to by a temporary object on the \
stack upon returning to the caller. This will be a dangling reference}}
UntypedAllocaTest() : allocaPtr(__builtin_alloca(sizeof(int))) { UntypedAllocaTest() : allocaPtr(__builtin_alloca(sizeof(int))) {
// All good! // All good!
} }
@ -86,6 +89,9 @@ struct TypedAllocaTest1 {
TypedAllocaTest1() // expected-warning{{1 uninitialized field}} TypedAllocaTest1() // expected-warning{{1 uninitialized field}}
: allocaPtr(static_cast<int *>(__builtin_alloca(sizeof(int)))) {} : allocaPtr(static_cast<int *>(__builtin_alloca(sizeof(int)))) {}
// expected-warning-re@-2 {{Address of stack memory allocated by call to \
alloca() on line {{[0-9]+}} is still referred to by a temporary object on the \
stack upon returning to the caller. This will be a dangling reference}}
}; };
void fTypedAllocaTest1() { void fTypedAllocaTest1() {
@ -96,6 +102,9 @@ struct TypedAllocaTest2 {
int *allocaPtr; int *allocaPtr;
int dontGetFilteredByNonPedanticMode = 0; int dontGetFilteredByNonPedanticMode = 0;
// expected-warning-re@+5 {{Address of stack memory allocated by call to \
alloca() on line {{[0-9]+}} is still referred to by a temporary object on the \
stack upon returning to the caller. This will be a dangling reference}}
TypedAllocaTest2() TypedAllocaTest2()
: allocaPtr(static_cast<int *>(__builtin_alloca(sizeof(int)))) { : allocaPtr(static_cast<int *>(__builtin_alloca(sizeof(int)))) {
*allocaPtr = 55555; *allocaPtr = 55555;
@ -341,6 +350,9 @@ class VoidPointerRRefTest1 {
void *&&vptrrref; // expected-note {{here}} void *&&vptrrref; // expected-note {{here}}
public: public:
// expected-warning@+3 {{Address of stack memory associated with local \
variable 'vptr' is still referred to by a temporary object on the stack \
upon returning to the caller. This will be a dangling reference}}
VoidPointerRRefTest1(void *vptr, char) : vptrrref(static_cast<void *&&>(vptr)) { // expected-warning {{binding reference member 'vptrrref' to stack allocated parameter 'vptr'}} VoidPointerRRefTest1(void *vptr, char) : vptrrref(static_cast<void *&&>(vptr)) { // expected-warning {{binding reference member 'vptrrref' to stack allocated parameter 'vptr'}}
// All good! // All good!
} }
@ -355,6 +367,9 @@ class VoidPointerRRefTest2 {
void **&&vpptrrref; // expected-note {{here}} void **&&vpptrrref; // expected-note {{here}}
public: public:
// expected-warning@+3 {{Address of stack memory associated with local \
variable 'vptr' is still referred to by a temporary object on the stack \
upon returning to the caller. This will be a dangling reference}}
VoidPointerRRefTest2(void **vptr, char) : vpptrrref(static_cast<void **&&>(vptr)) { // expected-warning {{binding reference member 'vpptrrref' to stack allocated parameter 'vptr'}} VoidPointerRRefTest2(void **vptr, char) : vpptrrref(static_cast<void **&&>(vptr)) { // expected-warning {{binding reference member 'vpptrrref' to stack allocated parameter 'vptr'}}
// All good! // All good!
} }
@ -369,6 +384,9 @@ class VoidPointerLRefTest {
void *&vptrrref; // expected-note {{here}} void *&vptrrref; // expected-note {{here}}
public: public:
// expected-warning@+3 {{Address of stack memory associated with local \
variable 'vptr' is still referred to by a temporary object on the stack \
upon returning to the caller. This will be a dangling reference}}
VoidPointerLRefTest(void *vptr, char) : vptrrref(static_cast<void *&>(vptr)) { // expected-warning {{binding reference member 'vptrrref' to stack allocated parameter 'vptr'}} VoidPointerLRefTest(void *vptr, char) : vptrrref(static_cast<void *&>(vptr)) { // expected-warning {{binding reference member 'vptrrref' to stack allocated parameter 'vptr'}}
// All good! // All good!
} }

View file

@ -5,6 +5,9 @@ void clang_analyzer_eval(int);
void callee(void **p) { void callee(void **p) {
int x; int x;
*p = &x; *p = &x;
// expected-warning@-1 {{Address of stack memory associated with local \
variable 'x' is still referred to by the stack variable 'arr' upon \
returning to the caller}}
} }
void loop() { void loop() {

View file

@ -137,3 +137,28 @@ namespace rdar13296133 {
} }
} }
void write_stack_address_to(char **q) {
char local;
*q = &local;
// expected-warning@-1 {{Address of stack memory associated with local \
variable 'local' is still referred to by the stack variable 'p' upon \
returning to the caller}}
}
void test_stack() {
char *p;
write_stack_address_to(&p);
}
struct C {
~C() {} // non-trivial class
};
C make1() {
C c;
return c; // no-warning
}
void test_copy_elision() {
C c1 = make1();
}