Skip to content

Commit ff9b296

Browse files
ivanmurashkosteakhalNagyDonat
authored
[analyzer] MallocChecker – Fix false positive leak for smart pointers in temporary objects (#152751)
Fixes PR60896 - false positive leak reports in various smart pointer scenarios including temporaries, inheritance, direct constructor calls, and mixed ownership patterns. Previously, the analyzer had no smart pointer handling in `checkPostCall`, causing it to report false positive leaks for memory properly managed by smart pointers while missing legitimate raw pointer leaks. --------- Co-authored-by: Balazs Benics <[email protected]> Co-authored-by: Donát Nagy <[email protected]>
1 parent 7115a0b commit ff9b296

File tree

2 files changed

+557
-2
lines changed

2 files changed

+557
-2
lines changed

clang/lib/StaticAnalyzer/Checkers/MallocChecker.cpp

Lines changed: 305 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@
7878
#include "clang/StaticAnalyzer/Core/PathSensitive/SVals.h"
7979
#include "clang/StaticAnalyzer/Core/PathSensitive/SymbolManager.h"
8080
#include "llvm/ADT/STLExtras.h"
81+
#include "llvm/ADT/SmallVector.h"
8182
#include "llvm/ADT/StringExtras.h"
8283
#include "llvm/Support/Casting.h"
8384
#include "llvm/Support/Compiler.h"
@@ -421,6 +422,13 @@ class MallocChecker
421422
void checkPreCall(const CallEvent &Call, CheckerContext &C) const;
422423
void checkPostCall(const CallEvent &Call, CheckerContext &C) const;
423424
bool evalCall(const CallEvent &Call, CheckerContext &C) const;
425+
426+
ProgramStateRef
427+
handleSmartPointerConstructorArguments(const CallEvent &Call,
428+
ProgramStateRef State) const;
429+
ProgramStateRef handleSmartPointerRelatedCalls(const CallEvent &Call,
430+
CheckerContext &C,
431+
ProgramStateRef State) const;
424432
void checkNewAllocator(const CXXAllocatorCall &Call, CheckerContext &C) const;
425433
void checkPostObjCMessage(const ObjCMethodCall &Call, CheckerContext &C) const;
426434
void checkPostStmt(const BlockExpr *BE, CheckerContext &C) const;
@@ -1096,6 +1104,54 @@ class StopTrackingCallback final : public SymbolVisitor {
10961104
return true;
10971105
}
10981106
};
1107+
1108+
/// EscapeTrackedCallback - A SymbolVisitor that marks allocated symbols as
1109+
/// escaped.
1110+
///
1111+
/// This visitor is used to suppress false positive leak reports when smart
1112+
/// pointers are nested in temporary objects passed by value to functions. When
1113+
/// the analyzer can't see the destructor calls for temporary objects, it may
1114+
/// incorrectly report leaks for memory that will be properly freed by the smart
1115+
/// pointer destructors.
1116+
///
1117+
/// The visitor traverses reachable symbols from a given set of memory regions
1118+
/// (typically smart pointer field regions) and marks any allocated symbols as
1119+
/// escaped. Escaped symbols are not reported as leaks by checkDeadSymbols.
1120+
class EscapeTrackedCallback final : public SymbolVisitor {
1121+
ProgramStateRef State;
1122+
1123+
explicit EscapeTrackedCallback(ProgramStateRef S) : State(std::move(S)) {}
1124+
1125+
public:
1126+
bool VisitSymbol(SymbolRef Sym) override {
1127+
if (const RefState *RS = State->get<RegionState>(Sym)) {
1128+
if (RS->isAllocated() || RS->isAllocatedOfSizeZero()) {
1129+
State = State->set<RegionState>(Sym, RefState::getEscaped(RS));
1130+
}
1131+
}
1132+
return true;
1133+
}
1134+
1135+
/// Escape tracked regions reachable from the given roots.
1136+
static ProgramStateRef
1137+
EscapeTrackedRegionsReachableFrom(ArrayRef<const MemRegion *> Roots,
1138+
ProgramStateRef State) {
1139+
if (Roots.empty())
1140+
return State;
1141+
1142+
// scanReachableSymbols is expensive, so we use a single visitor for all
1143+
// roots
1144+
SmallVector<const MemRegion *, 10> Regions;
1145+
EscapeTrackedCallback Visitor(State);
1146+
for (const MemRegion *R : Roots) {
1147+
Regions.push_back(R);
1148+
}
1149+
State->scanReachableSymbols(Regions, Visitor);
1150+
return Visitor.State;
1151+
}
1152+
1153+
friend class SymbolVisitor;
1154+
};
10991155
} // end anonymous namespace
11001156

11011157
static bool isStandardNew(const FunctionDecl *FD) {
@@ -3068,12 +3124,260 @@ void MallocChecker::checkDeadSymbols(SymbolReaper &SymReaper,
30683124
C.addTransition(state->set<RegionState>(RS), N);
30693125
}
30703126

3127+
// Allowlist of owning smart pointers we want to recognize.
3128+
// Start with unique_ptr and shared_ptr; weak_ptr is excluded intentionally
3129+
// because it does not own the pointee.
3130+
static bool isSmartPtrName(StringRef Name) {
3131+
return Name == "unique_ptr" || Name == "shared_ptr";
3132+
}
3133+
3134+
// Check if a type is a smart owning pointer type.
3135+
static bool isSmartPtrType(QualType QT) {
3136+
QT = QT->getCanonicalTypeUnqualified();
3137+
3138+
if (const auto *TST = QT->getAs<TemplateSpecializationType>()) {
3139+
const TemplateDecl *TD = TST->getTemplateName().getAsTemplateDecl();
3140+
if (!TD)
3141+
return false;
3142+
3143+
const auto *ND = dyn_cast_or_null<NamedDecl>(TD->getTemplatedDecl());
3144+
if (!ND)
3145+
return false;
3146+
3147+
// For broader coverage we recognize all template classes with names that
3148+
// match the allowlist even if they are not declared in namespace 'std'.
3149+
return isSmartPtrName(ND->getName());
3150+
}
3151+
3152+
return false;
3153+
}
3154+
3155+
/// Helper struct for collecting smart owning pointer field regions.
3156+
/// This allows both hasSmartPtrField and
3157+
/// collectSmartPtrFieldRegions to share the same traversal logic,
3158+
/// ensuring consistency.
3159+
struct FieldConsumer {
3160+
const MemRegion *Reg;
3161+
CheckerContext *C;
3162+
llvm::SmallPtrSetImpl<const MemRegion *> *Out;
3163+
3164+
FieldConsumer(const MemRegion *Reg, CheckerContext &C,
3165+
llvm::SmallPtrSetImpl<const MemRegion *> &Out)
3166+
: Reg(Reg), C(&C), Out(&Out) {}
3167+
3168+
void consume(const FieldDecl *FD) {
3169+
SVal L = C->getState()->getLValue(FD, loc::MemRegionVal(Reg));
3170+
if (const MemRegion *FR = L.getAsRegion())
3171+
Out->insert(FR);
3172+
}
3173+
3174+
std::optional<FieldConsumer> switchToBase(const CXXRecordDecl *BaseDecl,
3175+
bool IsVirtual) {
3176+
// Get the base class region
3177+
SVal BaseL =
3178+
C->getState()->getLValue(BaseDecl, Reg->getAs<SubRegion>(), IsVirtual);
3179+
if (const MemRegion *BaseObjRegion = BaseL.getAsRegion()) {
3180+
// Return a consumer for the base class
3181+
return FieldConsumer{BaseObjRegion, *C, *Out};
3182+
}
3183+
return std::nullopt;
3184+
}
3185+
};
3186+
3187+
/// Check if a record type has smart owning pointer fields (directly or in base
3188+
/// classes). When FC is provided, also collect the field regions.
3189+
///
3190+
/// This function has dual behavior:
3191+
/// - When FC is nullopt: Returns true if smart pointer fields are found
3192+
/// - When FC is provided: Always returns false, but collects field regions
3193+
/// as a side effect through the FieldConsumer
3194+
///
3195+
/// Note: When FC is provided, the return value should be ignored since the
3196+
/// function performs full traversal for collection and always returns false
3197+
/// to avoid early termination.
3198+
static bool hasSmartPtrField(const CXXRecordDecl *CRD,
3199+
std::optional<FieldConsumer> FC = std::nullopt) {
3200+
// Check direct fields
3201+
for (const FieldDecl *FD : CRD->fields()) {
3202+
if (isSmartPtrType(FD->getType())) {
3203+
if (!FC)
3204+
return true;
3205+
FC->consume(FD);
3206+
}
3207+
}
3208+
3209+
// Check fields from base classes
3210+
for (const CXXBaseSpecifier &BaseSpec : CRD->bases()) {
3211+
if (const CXXRecordDecl *BaseDecl =
3212+
BaseSpec.getType()->getAsCXXRecordDecl()) {
3213+
std::optional<FieldConsumer> NewFC;
3214+
if (FC) {
3215+
NewFC = FC->switchToBase(BaseDecl, BaseSpec.isVirtual());
3216+
if (!NewFC)
3217+
continue;
3218+
}
3219+
bool Found = hasSmartPtrField(BaseDecl, NewFC);
3220+
if (Found && !FC)
3221+
return true;
3222+
}
3223+
}
3224+
return false;
3225+
}
3226+
3227+
/// Check if an expression is an rvalue record type passed by value.
3228+
static bool isRvalueByValueRecord(const Expr *AE) {
3229+
if (AE->isGLValue())
3230+
return false;
3231+
3232+
QualType T = AE->getType();
3233+
if (!T->isRecordType() || T->isReferenceType())
3234+
return false;
3235+
3236+
// Accept common temp/construct forms but don't overfit.
3237+
return isa<CXXTemporaryObjectExpr, MaterializeTemporaryExpr, CXXConstructExpr,
3238+
InitListExpr, ImplicitCastExpr, CXXBindTemporaryExpr>(AE);
3239+
}
3240+
3241+
/// Check if an expression is an rvalue record with smart owning pointer fields
3242+
/// passed by value.
3243+
static bool isRvalueByValueRecordWithSmartPtr(const Expr *AE) {
3244+
if (!isRvalueByValueRecord(AE))
3245+
return false;
3246+
3247+
const auto *CRD = AE->getType()->getAsCXXRecordDecl();
3248+
return CRD && hasSmartPtrField(CRD);
3249+
}
3250+
3251+
/// Check if a CXXRecordDecl has a name matching recognized smart pointer names.
3252+
static bool isSmartPtrRecord(const CXXRecordDecl *RD) {
3253+
if (!RD)
3254+
return false;
3255+
3256+
// Check the record name directly and accept both std and custom smart pointer
3257+
// implementations for broader coverage
3258+
return isSmartPtrName(RD->getName());
3259+
}
3260+
3261+
/// Check if a call is a constructor of a smart owning pointer class that
3262+
/// accepts pointer parameters.
3263+
static bool isSmartPtrCall(const CallEvent &Call) {
3264+
// Only check for smart pointer constructor calls
3265+
const auto *CD = dyn_cast_or_null<CXXConstructorDecl>(Call.getDecl());
3266+
if (!CD)
3267+
return false;
3268+
3269+
const auto *RD = CD->getParent();
3270+
if (!isSmartPtrRecord(RD))
3271+
return false;
3272+
3273+
// Check if constructor takes a pointer parameter
3274+
for (const auto *Param : CD->parameters()) {
3275+
QualType ParamType = Param->getType();
3276+
if (ParamType->isPointerType() && !ParamType->isFunctionPointerType() &&
3277+
!ParamType->isVoidPointerType()) {
3278+
return true;
3279+
}
3280+
}
3281+
3282+
return false;
3283+
}
3284+
3285+
/// Collect memory regions of smart owning pointer fields from a record type
3286+
/// (including fields from base classes).
3287+
static void
3288+
collectSmartPtrFieldRegions(const MemRegion *Reg, QualType RecQT,
3289+
CheckerContext &C,
3290+
llvm::SmallPtrSetImpl<const MemRegion *> &Out) {
3291+
if (!Reg)
3292+
return;
3293+
3294+
const auto *CRD = RecQT->getAsCXXRecordDecl();
3295+
if (!CRD)
3296+
return;
3297+
3298+
FieldConsumer FC{Reg, C, Out};
3299+
hasSmartPtrField(CRD, FC);
3300+
}
3301+
3302+
/// Handle smart pointer constructor calls by escaping allocated symbols
3303+
/// that are passed as pointer arguments to the constructor.
3304+
ProgramStateRef MallocChecker::handleSmartPointerConstructorArguments(
3305+
const CallEvent &Call, ProgramStateRef State) const {
3306+
const auto *CD = cast<CXXConstructorDecl>(Call.getDecl());
3307+
for (unsigned I = 0, E = std::min(Call.getNumArgs(), CD->getNumParams());
3308+
I != E; ++I) {
3309+
const Expr *ArgExpr = Call.getArgExpr(I);
3310+
if (!ArgExpr)
3311+
continue;
3312+
3313+
QualType ParamType = CD->getParamDecl(I)->getType();
3314+
if (ParamType->isPointerType() && !ParamType->isFunctionPointerType() &&
3315+
!ParamType->isVoidPointerType()) {
3316+
// This argument is a pointer being passed to smart pointer constructor
3317+
SVal ArgVal = Call.getArgSVal(I);
3318+
SymbolRef Sym = ArgVal.getAsSymbol();
3319+
if (Sym && State->contains<RegionState>(Sym)) {
3320+
const RefState *RS = State->get<RegionState>(Sym);
3321+
if (RS && (RS->isAllocated() || RS->isAllocatedOfSizeZero())) {
3322+
State = State->set<RegionState>(Sym, RefState::getEscaped(RS));
3323+
}
3324+
}
3325+
}
3326+
}
3327+
return State;
3328+
}
3329+
3330+
/// Handle all smart pointer related processing in function calls.
3331+
/// This includes both direct smart pointer constructor calls and by-value
3332+
/// arguments containing smart pointer fields.
3333+
ProgramStateRef MallocChecker::handleSmartPointerRelatedCalls(
3334+
const CallEvent &Call, CheckerContext &C, ProgramStateRef State) const {
3335+
3336+
// Handle direct smart pointer constructor calls first
3337+
if (isSmartPtrCall(Call)) {
3338+
return handleSmartPointerConstructorArguments(Call, State);
3339+
}
3340+
3341+
// Handle smart pointer fields in by-value record arguments
3342+
llvm::SmallPtrSet<const MemRegion *, 8> SmartPtrFieldRoots;
3343+
for (unsigned I = 0, E = Call.getNumArgs(); I != E; ++I) {
3344+
const Expr *AE = Call.getArgExpr(I);
3345+
if (!AE)
3346+
continue;
3347+
AE = AE->IgnoreParenImpCasts();
3348+
3349+
if (!isRvalueByValueRecordWithSmartPtr(AE))
3350+
continue;
3351+
3352+
// Find a region for the argument.
3353+
SVal ArgVal = Call.getArgSVal(I);
3354+
const MemRegion *ArgRegion = ArgVal.getAsRegion();
3355+
// Collect direct smart owning pointer field regions
3356+
collectSmartPtrFieldRegions(ArgRegion, AE->getType(), C,
3357+
SmartPtrFieldRoots);
3358+
}
3359+
3360+
// Escape symbols reachable from smart pointer fields
3361+
if (!SmartPtrFieldRoots.empty()) {
3362+
SmallVector<const MemRegion *, 8> SmartPtrFieldRootsVec(
3363+
SmartPtrFieldRoots.begin(), SmartPtrFieldRoots.end());
3364+
State = EscapeTrackedCallback::EscapeTrackedRegionsReachableFrom(
3365+
SmartPtrFieldRootsVec, State);
3366+
}
3367+
3368+
return State;
3369+
}
3370+
30713371
void MallocChecker::checkPostCall(const CallEvent &Call,
30723372
CheckerContext &C) const {
3373+
// Handle existing post-call handlers first
30733374
if (const auto *PostFN = PostFnMap.lookup(Call)) {
30743375
(*PostFN)(this, C.getState(), Call, C);
3075-
return;
3376+
return; // Post-handler already called addTransition, we're done
30763377
}
3378+
3379+
// Handle smart pointer related processing only if no post-handler was called
3380+
C.addTransition(handleSmartPointerRelatedCalls(Call, C, C.getState()));
30773381
}
30783382

30793383
void MallocChecker::checkPreCall(const CallEvent &Call,
@@ -3194,7 +3498,6 @@ void MallocChecker::checkEscapeOnReturn(const ReturnStmt *S,
31943498
if (!Sym)
31953499
// If we are returning a field of the allocated struct or an array element,
31963500
// the callee could still free the memory.
3197-
// TODO: This logic should be a part of generic symbol escape callback.
31983501
if (const MemRegion *MR = RetVal.getAsRegion())
31993502
if (isa<FieldRegion, ElementRegion>(MR))
32003503
if (const SymbolicRegion *BMR =

0 commit comments

Comments
 (0)