Skip to content
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
0674909
wip
ashfordium Jan 27, 2025
0a26f83
wip
ashfordium Jan 29, 2025
3ebf222
Changes to checkPreStmt
ashfordium Jan 30, 2025
17bf250
start separating checkEndFunction and checkPreStmt
ashfordium Jan 30, 2025
89faf66
changes to checkEndFunction
ashfordium Jan 30, 2025
f225a2d
wip
ashfordium Jan 30, 2025
5816e91
wip, fixed lambdas test failures
ashfordium Feb 3, 2025
27c6cf8
formatting
ashfordium Feb 3, 2025
b73ad74
some cleanup
ashfordium Feb 3, 2025
13eac11
emit returned by part added
ashfordium Feb 3, 2025
129777e
add back IsConstructExpr bool in filtering
ashfordium Feb 3, 2025
adb78d1
wip
ashfordium Feb 3, 2025
2c62960
cleanup
ashfordium Feb 3, 2025
e43fb34
fix objc stack-allocated-capturing block return
ashfordium Feb 3, 2025
8c39d66
why does this work
ashfordium Feb 4, 2025
76493d1
double up these test expectations
ashfordium Feb 4, 2025
82400a7
check everytime visitor adds to escaped region
ashfordium Feb 4, 2025
39df282
fix comments
ashfordium Feb 4, 2025
65bee7a
formatting
ashfordium Feb 4, 2025
47f3b40
remove old includes
ashfordium Feb 4, 2025
6a2a01f
restore whitespace at end of file
ashfordium Feb 4, 2025
30ea259
add basic int* and int return assign expr tests
ashfordium Feb 4, 2025
567c8e6
return comma-separated expression
ashfordium Feb 4, 2025
4d4d537
Add expected warning to copy-elision.cpp no-elide tests
ashfordium Feb 7, 2025
a4ad3cb
Add simple true negative test case from code review
ashfordium Feb 7, 2025
8603e15
Remove temp object check in return expr checking
ashfordium Feb 7, 2025
7953dba
Add newline end of file
ashfordium Feb 7, 2025
51f8e2e
Add testcase: return of unknown symbol on stack
ashfordium Feb 7, 2025
029d122
Add testcase of false negative returning symbolic arg
ashfordium Feb 7, 2025
62ef423
Add testcase: bind rval ref to temporary
ashfordium Feb 8, 2025
e69ea53
Add testcases: return argptr, conditional expressions, comparison exp…
ashfordium Feb 9, 2025
9d4379e
Add testscases from SemaCXX/return-stack-addr.cpp
ashfordium Feb 10, 2025
1d6e363
Add lifetimebound attribute test case
ashfordium Feb 10, 2025
b679462
Add TODO on testcase for musttail attribute
ashfordium Feb 10, 2025
571f8ee
Add false negative comments
ashfordium Feb 10, 2025
6fff293
Take references to SmallVectorImpl rather than SmallVector
ashfordium Feb 10, 2025
9125ed8
formatting
ashfordium Feb 10, 2025
bb1b70e
Add newline at end of file
ashfordium Feb 10, 2025
16e3dc2
Add newline at end of file
ashfordium Feb 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
188 changes: 135 additions & 53 deletions clang/lib/StaticAnalyzer/Checkers/StackAddrEscapeChecker.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ class StackAddrEscapeChecker
CheckerContext &C) const;
void checkAsyncExecutedBlockCaptures(const BlockDataRegion &B,
CheckerContext &C) const;
void EmitStackError(CheckerContext &C, const MemRegion *R,
const Expr *RetE) const;
void EmitReturnLeakError(CheckerContext &C, const MemRegion *LeakedRegion,
const Expr *RetE) const;
bool isSemaphoreCaptured(const BlockDecl &B) const;
static SourceRange genName(raw_ostream &os, const MemRegion *R,
ASTContext &Ctx);
Expand Down Expand Up @@ -147,21 +147,38 @@ StackAddrEscapeChecker::getCapturedStackRegions(const BlockDataRegion &B,
return Regions;
}

void StackAddrEscapeChecker::EmitStackError(CheckerContext &C,
const MemRegion *R,
const Expr *RetE) const {
static void EmitReturnedAsPartOfError(llvm::raw_ostream &OS, SVal ReturnedVal,
const MemRegion *LeakedRegion) {
if (const MemRegion *ReturnedRegion = ReturnedVal.getAsRegion()) {
if (isa<BlockDataRegion>(ReturnedRegion)) {
OS << " is captured by a returned block";
return;
}
}

// Generic message
OS << " returned to caller";
}

void StackAddrEscapeChecker::EmitReturnLeakError(CheckerContext &C,
const MemRegion *R,
const Expr *RetE) const {
ExplodedNode *N = C.generateNonFatalErrorNode();
if (!N)
return;
if (!BT_returnstack)
BT_returnstack = std::make_unique<BugType>(
CheckNames[CK_StackAddrEscapeChecker],
"Return of address to stack-allocated memory");

// Generate a report for this bug.
SmallString<128> buf;
llvm::raw_svector_ostream os(buf);

// Error message formatting
SourceRange range = genName(os, R, C.getASTContext());
os << " returned to caller";
EmitReturnedAsPartOfError(os, C.getSVal(RetE), R);

auto report =
std::make_unique<PathSensitiveBugReport>(*BT_returnstack, os.str(), N);
report->addRange(RetE->getSourceRange());
Expand Down Expand Up @@ -209,30 +226,6 @@ void StackAddrEscapeChecker::checkAsyncExecutedBlockCaptures(
}
}

void StackAddrEscapeChecker::checkReturnedBlockCaptures(
const BlockDataRegion &B, CheckerContext &C) const {
for (const MemRegion *Region : getCapturedStackRegions(B, C)) {
if (isNotInCurrentFrame(Region, C))
continue;
ExplodedNode *N = C.generateNonFatalErrorNode();
if (!N)
continue;
if (!BT_capturedstackret)
BT_capturedstackret = std::make_unique<BugType>(
CheckNames[CK_StackAddrEscapeChecker],
"Address of stack-allocated memory is captured");
SmallString<128> Buf;
llvm::raw_svector_ostream Out(Buf);
SourceRange Range = genName(Out, Region, C.getASTContext());
Out << " is captured by a returned block";
auto Report = std::make_unique<PathSensitiveBugReport>(*BT_capturedstackret,
Out.str(), N);
if (Range.isValid())
Report->addRange(Range);
C.emitReport(std::move(Report));
}
}

void StackAddrEscapeChecker::checkPreCall(const CallEvent &Call,
CheckerContext &C) const {
if (!ChecksEnabled[CK_StackAddrAsyncEscapeChecker])
Expand All @@ -247,45 +240,134 @@ void StackAddrEscapeChecker::checkPreCall(const CallEvent &Call,
}
}

void StackAddrEscapeChecker::checkPreStmt(const ReturnStmt *RS,
CheckerContext &C) const {
if (!ChecksEnabled[CK_StackAddrEscapeChecker])
return;
/// A visitor made for use with a ScanReachableSymbols scanner, used
/// for finding stack regions within an SVal that live on the current
/// stack frame of the given checker context. This visitor excludes
/// NonParamVarRegion that data is bound to in a BlockDataRegion's
/// bindings, since these are likely uninteresting, e.g., in case a
/// temporary is constructed on the stack, but it captures values
/// that would leak.
class FindStackRegionsSymbolVisitor final : public SymbolVisitor {
CheckerContext &Ctxt;
const StackFrameContext *StackFrameContext;
SmallVector<const MemRegion *> &EscapingStackRegions;

const Expr *RetE = RS->getRetValue();
if (!RetE)
return;
RetE = RetE->IgnoreParens();
public:
explicit FindStackRegionsSymbolVisitor(
CheckerContext &Ctxt,
SmallVector<const MemRegion *> &StorageForStackRegions)
: Ctxt(Ctxt), StackFrameContext(Ctxt.getStackFrame()),
EscapingStackRegions(StorageForStackRegions) {}

SVal V = C.getSVal(RetE);
const MemRegion *R = V.getAsRegion();
if (!R)
return;
bool VisitSymbol(SymbolRef sym) override { return true; }

if (const BlockDataRegion *B = dyn_cast<BlockDataRegion>(R))
checkReturnedBlockCaptures(*B, C);
bool VisitMemRegion(const MemRegion *MR) override {
SaveIfEscapes(MR);

if (!isa<StackSpaceRegion>(R->getMemorySpace()) || isNotInCurrentFrame(R, C))
return;
if (const BlockDataRegion *BDR = MR->getAs<BlockDataRegion>())
return VisitBlockDataRegionCaptures(BDR);

return true;
}

private:
void SaveIfEscapes(const MemRegion *MR) {
const StackSpaceRegion *SSR =
MR->getMemorySpace()->getAs<StackSpaceRegion>();
if (SSR && SSR->getStackFrame() == StackFrameContext)
EscapingStackRegions.push_back(MR);
}

bool VisitBlockDataRegionCaptures(const BlockDataRegion *BDR) {
for (auto Var : BDR->referenced_vars()) {
SVal Val = Ctxt.getState()->getSVal(Var.getCapturedRegion());
const MemRegion *Region = Val.getAsRegion();
if (Region) {
SaveIfEscapes(Region);
VisitMemRegion(Region);
}
}

return false;
}
};

/// Given some memory regions that are flagged by FindStackRegionsSymbolVisitor,
/// this function filters out memory regions that are being returned that are
/// likely not true leaks:
/// 1. If returning a block data region that has stack memory space
/// 2. If returning a constructed object that has stack memory space
static SmallVector<const MemRegion *>
FilterReturnExpressionLeaks(const SmallVector<const MemRegion *> &MaybeEscaped,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: we usually take SmallVectorImpl by reference so the function works with SmallVector using any size as the inline buffer.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 6fff293.

CheckerContext &C, const Expr *RetE, SVal &RetVal) {

SmallVector<const MemRegion *> WillEscape;

const MemRegion *RetRegion = RetVal.getAsRegion();

// Returning a record by value is fine. (In this case, the returned
// expression will be a copy-constructor, possibly wrapped in an
// ExprWithCleanups node.)
if (const ExprWithCleanups *Cleanup = dyn_cast<ExprWithCleanups>(RetE))
RetE = Cleanup->getSubExpr();
if (isa<CXXConstructExpr>(RetE) && RetE->getType()->isRecordType())
return;
bool IsConstructExpr =
isa<CXXConstructExpr>(RetE) && RetE->getType()->isRecordType();

// The CK_CopyAndAutoreleaseBlockObject cast causes the block to be copied
// so the stack address is not escaping here.
bool IsCopyAndAutoreleaseBlockObj = false;
if (const auto *ICE = dyn_cast<ImplicitCastExpr>(RetE)) {
if (isa<BlockDataRegion>(R) &&
ICE->getCastKind() == CK_CopyAndAutoreleaseBlockObject) {
return;
}
IsCopyAndAutoreleaseBlockObj =
isa_and_nonnull<BlockDataRegion>(RetRegion) &&
ICE->getCastKind() == CK_CopyAndAutoreleaseBlockObject;
}

for (const MemRegion *MR : MaybeEscaped) {
if (RetRegion == MR && (IsCopyAndAutoreleaseBlockObj || IsConstructExpr))
continue;

// If this is a construct expr of an unelided return value copy, then don't
// warn about returning a region that currently lives on the stack.
if (IsConstructExpr && RetVal.getAs<nonloc::LazyCompoundVal>() &&
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am wondering if this is the right solution here. I am a bit concerned that there are some other cases that we missed here. I think we are only interested in stack memory regions that are behind a pointer.

But this is also not entirely true. Imagine the following case:

DoesDeepCopy myFunc() {
   int local;
   DoesDeepCopy l(&local);
   return l;
};

Here, if DoesDeepCopy's ctor would copy the value behind the pointer, the code above is fine. On the second though, I wonder if NRVO can actually kick in making this code unsafe.

That being said, if the copy ctor is actually invoked, hopefully we would observe the effects of that, the fields would get invalidated, and we would not see the memory region from the stack.

Overall, I think some scenarios here might need more tests.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line is only for removing warnings. It is a little loose and maybe could cause some false negatives. Is there a way here to relate the lazy compound val that would be returned and the temp object region in an example like this from copy-elision.cpp test:

template <typename T> struct AddressVector {
  T *buf[20];
  int len;

  AddressVector() : len(0) {}

  void push(T *t) {
    buf[len] = t;
    ++len;
  }
};

class ClassWithoutDestructor {
  AddressVector<ClassWithoutDestructor> &v;

public:
  ClassWithoutDestructor(AddressVector<ClassWithoutDestructor> &v) : v(v) {
    push();
  }

  ClassWithoutDestructor(ClassWithoutDestructor &&c) : v(c.v) { push(); }
  ClassWithoutDestructor(const ClassWithoutDestructor &c) : v(c.v) { push(); }

  void push() { v.push(this); }
};

ClassWithoutDestructor make1(AddressVector<ClassWithoutDestructor> &v) {
  return ClassWithoutDestructor(v); 
  // no-elide-warning@-1 {{Address of stack memory associated with temporary \
object of type 'ClassWithoutDestructor' is still \
referred to by the caller variable 'v' upon returning to the caller}}
}

Without some check like this, then the line with return ClassWithoutDestructor(v); will warn, and I think it should not under -analyzer-config elide-constructors=false -DNO_ELIDE_FLAG -std=c++11. Is there a better way to relate the lazy compound val of the return expr and the temp object region that is created?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we are only interested in stack memory regions that are behind a pointer.

I think there are still some cases where warning on the returned pointer is also wanted, like this one, or maybe more complicated versions of this that normal compilation warnings wouldn't catch

int *test() {
  int x = 14;
  return &x;
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The local memory region in the function test is behind a pointer so that is matches my description.

But we would not want to warn for:

int test2() {
  int x = 14;
  return x;
}

Because here x is not behind a pointer.

Could you elaborate why the analyzer thinks the temporary is referred to by v here?

Is there a better way to relate the lazy compound val of the return expr and the temp object region that is created?

Dealing with LazyCompoundVals can be a bit fiddly. You could take a look at FindUninitializedField in CallAndMessageChecker.cpp .

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall, I wonder if what we have here is similar to what the analyzer is doing when creating the checkPointerEscape callback. We basically want to collect all the pointers from the returned value a similar way and want to check if any of those point to stack memory. I think collecting the pointers might be an API that we need/want to expose to the checkers.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We basically want to collect all the pointers from the returned value a similar way and want to check if any of those point to stack memory.

That is what I was hoping this PR was: add a ScanReachableSymbols & SymbolVisitor that takes all stack regions behind the return SVal and warns on those. Previously, this checker only did this for BlockDataRegion captures and it only checked if the return SVal is a stack space region.

IIUC, you would like this instead to be reworked into checkPointerEscape?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does a v.push(this)

Ah, thanks for the explanation, I missed this detail somehow. So this actually is a true positive warning when copy elision is not happening. I think in that case we should only filter these temporaries out when the temporary is actually elided (we should see this in the AST).

And now I better understand what you mean by relating the returned memory region and the lazy compound val. You already have a RetRegion, would comparing that to LazyCompoundVal::getRegion() work?

Copy link
Collaborator

@Xazax-hun Xazax-hun Feb 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIUC, you would like this instead to be reworked into checkPointerEscape?

Nope, my bad. Sorry, I misunderstood something. Using ScanReachableSymbols sounds like the right approach to me. I am wondering why is it sufficient to check if a memory region belongs to the current stack. I.e., why we would not warn on:

int test2() {
  int x = someFunc();
  return x;
}

Copy link
Contributor Author

@ashfordium ashfordium Feb 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am wondering why is it sufficient to check if a memory region belongs to the current stack. I.e., why we would not warn on:

int test2() {
int x = someFunc();
 return x;
}

Is it because we do this:

void StackAddrEscapeChecker::checkPreStmt(const ReturnStmt *RS,
                                          CheckerContext &C) const {
  if (!ChecksEnabled[CK_StackAddrEscapeChecker])
    return;

  const Expr *RetE = RS->getRetValue();
  if (!RetE)
    return;
  RetE = RetE->IgnoreParens();

  SVal V = C.getSVal(RetE);

  ... scan V for escaping stack regions ...

we get the result of the LValueToRValue conversion?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we get the result of the LValueToRValue conversion?

Ah, I see. Thanks!

isa<CXXTempObjectRegion>(MR))
continue;

WillEscape.push_back(MR);
}

EmitStackError(C, R, RetE);
return WillEscape;
}

/// For use in finding regions that live on the checker context's current
/// stack frame, deep in the SVal representing the return value.
static SmallVector<const MemRegion *>
FindEscapingStackRegions(CheckerContext &C, const Expr *RetE, SVal RetVal) {
SmallVector<const MemRegion *> FoundStackRegions;

FindStackRegionsSymbolVisitor Finder(C, FoundStackRegions);
ScanReachableSymbols Scanner(C.getState(), Finder);
Scanner.scan(RetVal);

return FilterReturnExpressionLeaks(FoundStackRegions, C, RetE, RetVal);
}

void StackAddrEscapeChecker::checkPreStmt(const ReturnStmt *RS,
CheckerContext &C) const {
if (!ChecksEnabled[CK_StackAddrEscapeChecker])
return;

const Expr *RetE = RS->getRetValue();
if (!RetE)
return;
RetE = RetE->IgnoreParens();

SVal V = C.getSVal(RetE);

SmallVector<const MemRegion *> EscapedStackRegions =
FindEscapingStackRegions(C, RetE, V);

for (const MemRegion *ER : EscapedStackRegions)
EmitReturnLeakError(C, ER, RetE);
}

static const MemSpaceRegion *getStackOrGlobalSpaceRegion(const MemRegion *R) {
Expand Down
75 changes: 55 additions & 20 deletions clang/test/Analysis/stack-addr-ps.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ void* lambda_to_context_direct_pointer_uncalled() {
int local = 42;
p = &local; // no-warning: analyzed only as top-level, ignored explicitly by the checker
};
return new MyFunction(&lambda);
return new MyFunction(&lambda); // expected-warning{{Address of stack memory associated with local variable 'lambda' returned to caller}}
}

void lambda_to_context_direct_pointer_lifetime_extended() {
Expand Down Expand Up @@ -410,16 +410,16 @@ void** returned_arr_of_ptr_top() {
int* p = &local;
void** arr = new void*[2];
arr[1] = p;
return arr;
} // no-warning False Negative
return arr; // expected-warning{{Address of stack memory associated with local variable 'local' returned to caller}}
}

void** returned_arr_of_ptr_callee() {
int local = 42;
int* p = &local;
void** arr = new void*[2];
arr[1] = p;
return arr;
} // no-warning False Negative
return arr; // expected-warning{{Address of stack memory associated with local variable 'local' returned to caller}}
}

void returned_arr_of_ptr_caller() {
void** arr = returned_arr_of_ptr_callee();
Expand Down Expand Up @@ -466,16 +466,16 @@ void** returned_arr_of_ptr_top(int idx) {
int* p = &local;
void** arr = new void*[2];
arr[idx] = p;
return arr;
} // no-warning False Negative
return arr; // expected-warning{{Address of stack memory associated with local variable 'local' returned to caller}}
}

void** returned_arr_of_ptr_callee(int idx) {
int local = 42;
int* p = &local;
void** arr = new void*[2];
arr[idx] = p;
return arr;
} // no-warning False Negative
return arr; // expected-warning{{Address of stack memory associated with local variable 'local' returned to caller}}
}

void returned_arr_of_ptr_caller(int idx) {
void** arr = returned_arr_of_ptr_callee(idx);
Expand Down Expand Up @@ -525,14 +525,25 @@ S returned_struct_with_ptr_top() {
int local = 42;
S s;
s.p = &local;
return s;
} // no-warning False Negative, requires traversing returned LazyCompoundVals
return s; // expected-warning{{Address of stack memory associated with local variable 'local' returned to caller}}
}

S returned_struct_with_ptr_callee() {
int local = 42;
S s;
s.p = &local;
return s; // expected-warning{{'local' is still referred to by the caller variable 's'}}
return s; // expected-warning {{Address of stack memory associated with local variable 'local' returned to caller}} expected-warning{{Address of stack memory associated with local variable 'local' is still referred to by the caller variable 's' upon returning to the caller. This will be a dangling reference}}
}

S leak_through_field_of_returned_object() {
int local = 14;
S s{&local};
return s; // expected-warning {{Address of stack memory associated with local variable 'local' returned to caller}}
}

S leak_through_compound_literal() {
int local = 0;
return (S) { &local }; // expected-warning {{Address of stack memory associated with local variable 'local' returned to caller}}
}

void returned_struct_with_ptr_caller() {
Expand All @@ -555,6 +566,30 @@ void static_struct_with_ptr() {
}
} // namespace leaking_via_struct_with_ptr

namespace leaking_via_nested_structs_with_ptr {
struct Inner {
int *ptr;
};

struct Outer {
Inner I;
};

struct Deriving : public Outer {};

Outer leaks_through_nested_objects() {
int local = 0;
Outer O{&local};
return O; // expected-warning {{Address of stack memory associated with local variable 'local' returned to caller}}
}

Deriving leaks_through_base_objects() {
int local = 0;
Deriving D{&local};
return D; // expected-warning {{Address of stack memory associated with local variable 'local' returned to caller}}
}
} // namespace leaking_via_nested_structs_with_ptr

namespace leaking_via_ref_to_struct_with_ptr {
struct S {
int* p;
Expand Down Expand Up @@ -613,15 +648,15 @@ S* returned_ptr_to_struct_with_ptr_top() {
int local = 42;
S* s = new S;
s->p = &local;
return s;
} // no-warning False Negative
return s; // expected-warning {{Address of stack memory associated with local variable 'local' returned to caller}}
}

S* returned_ptr_to_struct_with_ptr_callee() {
int local = 42;
S* s = new S;
s->p = &local;
return s;
} // no-warning False Negative
return s; // expected-warning {{Address of stack memory associated with local variable 'local' returned to caller}}
}

void returned_ptr_to_struct_with_ptr_caller() {
S* s = returned_ptr_to_struct_with_ptr_callee();
Expand Down Expand Up @@ -676,15 +711,15 @@ S* returned_ptr_to_struct_with_ptr_top() {
int local = 42;
S* s = new S[2];
s[1].p = &local;
return s;
} // no-warning False Negative
return s; // expected-warning {{Address of stack memory associated with local variable 'local' returned to caller}}
}

S* returned_ptr_to_struct_with_ptr_callee() {
int local = 42;
S* s = new S[2];
s[1].p = &local;
return s;
} // no-warning False Negative
return s; // expected-warning {{Address of stack memory associated with local variable 'local' returned to caller}}
}

void returned_ptr_to_struct_with_ptr_caller() {
S* s = returned_ptr_to_struct_with_ptr_callee();
Expand Down
Loading