| //==- CheckObjCDealloc.cpp - Check ObjC -dealloc implementation --*- C++ -*-==// |
| // |
| // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. |
| // See https://llvm.org/LICENSE.txt for license information. |
| // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception |
| // |
| //===----------------------------------------------------------------------===// |
| // |
| // This checker analyzes Objective-C -dealloc methods and their callees |
| // to warn about improper releasing of instance variables that back synthesized |
| // properties. It warns about missing releases in the following cases: |
| // - When a class has a synthesized instance variable for a 'retain' or 'copy' |
| // property and lacks a -dealloc method in its implementation. |
| // - When a class has a synthesized instance variable for a 'retain'/'copy' |
| // property but the ivar is not released in -dealloc by either -release |
| // or by nilling out the property. |
| // |
| // It warns about extra releases in -dealloc (but not in callees) when a |
| // synthesized instance variable is released in the following cases: |
| // - When the property is 'assign' and is not 'readonly'. |
| // - When the property is 'weak'. |
| // |
| // This checker only warns for instance variables synthesized to back |
| // properties. Handling the more general case would require inferring whether |
| // an instance variable is stored retained or not. For synthesized properties, |
| // this is specified in the property declaration itself. |
| // |
| //===----------------------------------------------------------------------===// |
| |
| #include "clang/StaticAnalyzer/Checkers/BuiltinCheckerRegistration.h" |
| #include "clang/Analysis/PathDiagnostic.h" |
| #include "clang/AST/Attr.h" |
| #include "clang/AST/DeclObjC.h" |
| #include "clang/AST/Expr.h" |
| #include "clang/AST/ExprObjC.h" |
| #include "clang/Basic/LangOptions.h" |
| #include "clang/Basic/TargetInfo.h" |
| #include "clang/StaticAnalyzer/Core/BugReporter/BugReporter.h" |
| #include "clang/StaticAnalyzer/Core/BugReporter/BugType.h" |
| #include "clang/StaticAnalyzer/Core/Checker.h" |
| #include "clang/StaticAnalyzer/Core/PathSensitive/AnalysisManager.h" |
| #include "clang/StaticAnalyzer/Core/PathSensitive/CallEvent.h" |
| #include "clang/StaticAnalyzer/Core/PathSensitive/CheckerContext.h" |
| #include "clang/StaticAnalyzer/Core/PathSensitive/ProgramState.h" |
| #include "clang/StaticAnalyzer/Core/PathSensitive/ProgramStateTrait.h" |
| #include "clang/StaticAnalyzer/Core/PathSensitive/SymbolManager.h" |
| #include "llvm/Support/raw_ostream.h" |
| |
| using namespace clang; |
| using namespace ento; |
| |
| /// Indicates whether an instance variable is required to be released in |
| /// -dealloc. |
| enum class ReleaseRequirement { |
| /// The instance variable must be released, either by calling |
| /// -release on it directly or by nilling it out with a property setter. |
| MustRelease, |
| |
| /// The instance variable must not be directly released with -release. |
| MustNotReleaseDirectly, |
| |
| /// The requirement for the instance variable could not be determined. |
| Unknown |
| }; |
| |
| /// Returns true if the property implementation is synthesized and the |
| /// type of the property is retainable. |
| static bool isSynthesizedRetainableProperty(const ObjCPropertyImplDecl *I, |
| const ObjCIvarDecl **ID, |
| const ObjCPropertyDecl **PD) { |
| |
| if (I->getPropertyImplementation() != ObjCPropertyImplDecl::Synthesize) |
| return false; |
| |
| (*ID) = I->getPropertyIvarDecl(); |
| if (!(*ID)) |
| return false; |
| |
| QualType T = (*ID)->getType(); |
| if (!T->isObjCRetainableType()) |
| return false; |
| |
| (*PD) = I->getPropertyDecl(); |
| // Shouldn't be able to synthesize a property that doesn't exist. |
| assert(*PD); |
| |
| return true; |
| } |
| |
| namespace { |
| |
| class ObjCDeallocChecker |
| : public Checker<check::ASTDecl<ObjCImplementationDecl>, |
| check::PreObjCMessage, check::PostObjCMessage, |
| check::PreCall, |
| check::BeginFunction, check::EndFunction, |
| eval::Assume, |
| check::PointerEscape, |
| check::PreStmt<ReturnStmt>> { |
| |
| mutable IdentifierInfo *NSObjectII, *SenTestCaseII, *XCTestCaseII, |
| *Block_releaseII, *CIFilterII; |
| |
| mutable Selector DeallocSel, ReleaseSel; |
| |
| std::unique_ptr<BugType> MissingReleaseBugType; |
| std::unique_ptr<BugType> ExtraReleaseBugType; |
| std::unique_ptr<BugType> MistakenDeallocBugType; |
| |
| public: |
| ObjCDeallocChecker(); |
| |
| void checkASTDecl(const ObjCImplementationDecl *D, AnalysisManager& Mgr, |
| BugReporter &BR) const; |
| void checkBeginFunction(CheckerContext &Ctx) const; |
| void checkPreObjCMessage(const ObjCMethodCall &M, CheckerContext &C) const; |
| void checkPreCall(const CallEvent &Call, CheckerContext &C) const; |
| void checkPostObjCMessage(const ObjCMethodCall &M, CheckerContext &C) const; |
| |
| ProgramStateRef evalAssume(ProgramStateRef State, SVal Cond, |
| bool Assumption) const; |
| |
| ProgramStateRef checkPointerEscape(ProgramStateRef State, |
| const InvalidatedSymbols &Escaped, |
| const CallEvent *Call, |
| PointerEscapeKind Kind) const; |
| void checkPreStmt(const ReturnStmt *RS, CheckerContext &C) const; |
| void checkEndFunction(const ReturnStmt *RS, CheckerContext &Ctx) const; |
| |
| private: |
| void diagnoseMissingReleases(CheckerContext &C) const; |
| |
| bool diagnoseExtraRelease(SymbolRef ReleasedValue, const ObjCMethodCall &M, |
| CheckerContext &C) const; |
| |
| bool diagnoseMistakenDealloc(SymbolRef DeallocedValue, |
| const ObjCMethodCall &M, |
| CheckerContext &C) const; |
| |
| SymbolRef getValueReleasedByNillingOut(const ObjCMethodCall &M, |
| CheckerContext &C) const; |
| |
| const ObjCIvarRegion *getIvarRegionForIvarSymbol(SymbolRef IvarSym) const; |
| SymbolRef getInstanceSymbolFromIvarSymbol(SymbolRef IvarSym) const; |
| |
| const ObjCPropertyImplDecl* |
| findPropertyOnDeallocatingInstance(SymbolRef IvarSym, |
| CheckerContext &C) const; |
| |
| ReleaseRequirement |
| getDeallocReleaseRequirement(const ObjCPropertyImplDecl *PropImpl) const; |
| |
| bool isInInstanceDealloc(const CheckerContext &C, SVal &SelfValOut) const; |
| bool isInInstanceDealloc(const CheckerContext &C, const LocationContext *LCtx, |
| SVal &SelfValOut) const; |
| bool instanceDeallocIsOnStack(const CheckerContext &C, |
| SVal &InstanceValOut) const; |
| |
| bool isSuperDeallocMessage(const ObjCMethodCall &M) const; |
| |
| const ObjCImplDecl *getContainingObjCImpl(const LocationContext *LCtx) const; |
| |
| const ObjCPropertyDecl * |
| findShadowedPropertyDecl(const ObjCPropertyImplDecl *PropImpl) const; |
| |
| void transitionToReleaseValue(CheckerContext &C, SymbolRef Value) const; |
| ProgramStateRef removeValueRequiringRelease(ProgramStateRef State, |
| SymbolRef InstanceSym, |
| SymbolRef ValueSym) const; |
| |
| void initIdentifierInfoAndSelectors(ASTContext &Ctx) const; |
| |
| bool classHasSeparateTeardown(const ObjCInterfaceDecl *ID) const; |
| |
| bool isReleasedByCIFilterDealloc(const ObjCPropertyImplDecl *PropImpl) const; |
| bool isNibLoadedIvarWithoutRetain(const ObjCPropertyImplDecl *PropImpl) const; |
| }; |
| } // End anonymous namespace. |
| |
| |
| /// Maps from the symbol for a class instance to the set of |
| /// symbols remaining that must be released in -dealloc. |
| REGISTER_SET_FACTORY_WITH_PROGRAMSTATE(SymbolSet, SymbolRef) |
| REGISTER_MAP_WITH_PROGRAMSTATE(UnreleasedIvarMap, SymbolRef, SymbolSet) |
| |
| |
| /// An AST check that diagnose when the class requires a -dealloc method and |
| /// is missing one. |
| void ObjCDeallocChecker::checkASTDecl(const ObjCImplementationDecl *D, |
| AnalysisManager &Mgr, |
| BugReporter &BR) const { |
| assert(Mgr.getLangOpts().getGC() != LangOptions::GCOnly); |
| assert(!Mgr.getLangOpts().ObjCAutoRefCount); |
| initIdentifierInfoAndSelectors(Mgr.getASTContext()); |
| |
| const ObjCInterfaceDecl *ID = D->getClassInterface(); |
| // If the class is known to have a lifecycle with a separate teardown method |
| // then it may not require a -dealloc method. |
| if (classHasSeparateTeardown(ID)) |
| return; |
| |
| // Does the class contain any synthesized properties that are retainable? |
| // If not, skip the check entirely. |
| const ObjCPropertyImplDecl *PropImplRequiringRelease = nullptr; |
| bool HasOthers = false; |
| for (const auto *I : D->property_impls()) { |
| if (getDeallocReleaseRequirement(I) == ReleaseRequirement::MustRelease) { |
| if (!PropImplRequiringRelease) |
| PropImplRequiringRelease = I; |
| else { |
| HasOthers = true; |
| break; |
| } |
| } |
| } |
| |
| if (!PropImplRequiringRelease) |
| return; |
| |
| const ObjCMethodDecl *MD = nullptr; |
| |
| // Scan the instance methods for "dealloc". |
| for (const auto *I : D->instance_methods()) { |
| if (I->getSelector() == DeallocSel) { |
| MD = I; |
| break; |
| } |
| } |
| |
| if (!MD) { // No dealloc found. |
| const char* Name = "Missing -dealloc"; |
| |
| std::string Buf; |
| llvm::raw_string_ostream OS(Buf); |
| OS << "'" << *D << "' lacks a 'dealloc' instance method but " |
| << "must release '" << *PropImplRequiringRelease->getPropertyIvarDecl() |
| << "'"; |
| |
| if (HasOthers) |
| OS << " and others"; |
| PathDiagnosticLocation DLoc = |
| PathDiagnosticLocation::createBegin(D, BR.getSourceManager()); |
| |
| BR.EmitBasicReport(D, this, Name, categories::CoreFoundationObjectiveC, |
| OS.str(), DLoc); |
| return; |
| } |
| } |
| |
| /// If this is the beginning of -dealloc, mark the values initially stored in |
| /// instance variables that must be released by the end of -dealloc |
| /// as unreleased in the state. |
| void ObjCDeallocChecker::checkBeginFunction( |
| CheckerContext &C) const { |
| initIdentifierInfoAndSelectors(C.getASTContext()); |
| |
| // Only do this if the current method is -dealloc. |
| SVal SelfVal; |
| if (!isInInstanceDealloc(C, SelfVal)) |
| return; |
| |
| SymbolRef SelfSymbol = SelfVal.getAsSymbol(); |
| |
| const LocationContext *LCtx = C.getLocationContext(); |
| ProgramStateRef InitialState = C.getState(); |
| |
| ProgramStateRef State = InitialState; |
| |
| SymbolSet::Factory &F = State->getStateManager().get_context<SymbolSet>(); |
| |
| // Symbols that must be released by the end of the -dealloc; |
| SymbolSet RequiredReleases = F.getEmptySet(); |
| |
| // If we're an inlined -dealloc, we should add our symbols to the existing |
| // set from our subclass. |
| if (const SymbolSet *CurrSet = State->get<UnreleasedIvarMap>(SelfSymbol)) |
| RequiredReleases = *CurrSet; |
| |
| for (auto *PropImpl : getContainingObjCImpl(LCtx)->property_impls()) { |
| ReleaseRequirement Requirement = getDeallocReleaseRequirement(PropImpl); |
| if (Requirement != ReleaseRequirement::MustRelease) |
| continue; |
| |
| SVal LVal = State->getLValue(PropImpl->getPropertyIvarDecl(), SelfVal); |
| Optional<Loc> LValLoc = LVal.getAs<Loc>(); |
| if (!LValLoc) |
| continue; |
| |
| SVal InitialVal = State->getSVal(LValLoc.getValue()); |
| SymbolRef Symbol = InitialVal.getAsSymbol(); |
| if (!Symbol || !isa<SymbolRegionValue>(Symbol)) |
| continue; |
| |
| // Mark the value as requiring a release. |
| RequiredReleases = F.add(RequiredReleases, Symbol); |
| } |
| |
| if (!RequiredReleases.isEmpty()) { |
| State = State->set<UnreleasedIvarMap>(SelfSymbol, RequiredReleases); |
| } |
| |
| if (State != InitialState) { |
| C.addTransition(State); |
| } |
| } |
| |
| /// Given a symbol for an ivar, return the ivar region it was loaded from. |
| /// Returns nullptr if the instance symbol cannot be found. |
| const ObjCIvarRegion * |
| ObjCDeallocChecker::getIvarRegionForIvarSymbol(SymbolRef IvarSym) const { |
| return dyn_cast_or_null<ObjCIvarRegion>(IvarSym->getOriginRegion()); |
| } |
| |
| /// Given a symbol for an ivar, return a symbol for the instance containing |
| /// the ivar. Returns nullptr if the instance symbol cannot be found. |
| SymbolRef |
| ObjCDeallocChecker::getInstanceSymbolFromIvarSymbol(SymbolRef IvarSym) const { |
| |
| const ObjCIvarRegion *IvarRegion = getIvarRegionForIvarSymbol(IvarSym); |
| if (!IvarRegion) |
| return nullptr; |
| |
| return IvarRegion->getSymbolicBase()->getSymbol(); |
| } |
| |
| /// If we are in -dealloc or -dealloc is on the stack, handle the call if it is |
| /// a release or a nilling-out property setter. |
| void ObjCDeallocChecker::checkPreObjCMessage( |
| const ObjCMethodCall &M, CheckerContext &C) const { |
| // Only run if -dealloc is on the stack. |
| SVal DeallocedInstance; |
| if (!instanceDeallocIsOnStack(C, DeallocedInstance)) |
| return; |
| |
| SymbolRef ReleasedValue = nullptr; |
| |
| if (M.getSelector() == ReleaseSel) { |
| ReleasedValue = M.getReceiverSVal().getAsSymbol(); |
| } else if (M.getSelector() == DeallocSel && !M.isReceiverSelfOrSuper()) { |
| if (diagnoseMistakenDealloc(M.getReceiverSVal().getAsSymbol(), M, C)) |
| return; |
| } |
| |
| if (ReleasedValue) { |
| // An instance variable symbol was released with -release: |
| // [_property release]; |
| if (diagnoseExtraRelease(ReleasedValue,M, C)) |
| return; |
| } else { |
| // An instance variable symbol was released nilling out its property: |
| // self.property = nil; |
| ReleasedValue = getValueReleasedByNillingOut(M, C); |
| } |
| |
| if (!ReleasedValue) |
| return; |
| |
| transitionToReleaseValue(C, ReleasedValue); |
| } |
| |
| /// If we are in -dealloc or -dealloc is on the stack, handle the call if it is |
| /// call to Block_release(). |
| void ObjCDeallocChecker::checkPreCall(const CallEvent &Call, |
| CheckerContext &C) const { |
| const IdentifierInfo *II = Call.getCalleeIdentifier(); |
| if (II != Block_releaseII) |
| return; |
| |
| if (Call.getNumArgs() != 1) |
| return; |
| |
| SymbolRef ReleasedValue = Call.getArgSVal(0).getAsSymbol(); |
| if (!ReleasedValue) |
| return; |
| |
| transitionToReleaseValue(C, ReleasedValue); |
| } |
| /// If the message was a call to '[super dealloc]', diagnose any missing |
| /// releases. |
| void ObjCDeallocChecker::checkPostObjCMessage( |
| const ObjCMethodCall &M, CheckerContext &C) const { |
| // We perform this check post-message so that if the super -dealloc |
| // calls a helper method and that this class overrides, any ivars released in |
| // the helper method will be recorded before checking. |
| if (isSuperDeallocMessage(M)) |
| diagnoseMissingReleases(C); |
| } |
| |
| /// Check for missing releases even when -dealloc does not call |
| /// '[super dealloc]'. |
| void ObjCDeallocChecker::checkEndFunction( |
| const ReturnStmt *RS, CheckerContext &C) const { |
| diagnoseMissingReleases(C); |
| } |
| |
| /// Check for missing releases on early return. |
| void ObjCDeallocChecker::checkPreStmt( |
| const ReturnStmt *RS, CheckerContext &C) const { |
| diagnoseMissingReleases(C); |
| } |
| |
| /// When a symbol is assumed to be nil, remove it from the set of symbols |
| /// require to be nil. |
| ProgramStateRef ObjCDeallocChecker::evalAssume(ProgramStateRef State, SVal Cond, |
| bool Assumption) const { |
| if (State->get<UnreleasedIvarMap>().isEmpty()) |
| return State; |
| |
| auto *CondBSE = dyn_cast_or_null<BinarySymExpr>(Cond.getAsSymbol()); |
| if (!CondBSE) |
| return State; |
| |
| BinaryOperator::Opcode OpCode = CondBSE->getOpcode(); |
| if (Assumption) { |
| if (OpCode != BO_EQ) |
| return State; |
| } else { |
| if (OpCode != BO_NE) |
| return State; |
| } |
| |
| SymbolRef NullSymbol = nullptr; |
| if (auto *SIE = dyn_cast<SymIntExpr>(CondBSE)) { |
| const llvm::APInt &RHS = SIE->getRHS(); |
| if (RHS != 0) |
| return State; |
| NullSymbol = SIE->getLHS(); |
| } else if (auto *SIE = dyn_cast<IntSymExpr>(CondBSE)) { |
| const llvm::APInt &LHS = SIE->getLHS(); |
| if (LHS != 0) |
| return State; |
| NullSymbol = SIE->getRHS(); |
| } else { |
| return State; |
| } |
| |
| SymbolRef InstanceSymbol = getInstanceSymbolFromIvarSymbol(NullSymbol); |
| if (!InstanceSymbol) |
| return State; |
| |
| State = removeValueRequiringRelease(State, InstanceSymbol, NullSymbol); |
| |
| return State; |
| } |
| |
| /// If a symbol escapes conservatively assume unseen code released it. |
| ProgramStateRef ObjCDeallocChecker::checkPointerEscape( |
| ProgramStateRef State, const InvalidatedSymbols &Escaped, |
| const CallEvent *Call, PointerEscapeKind Kind) const { |
| |
| if (State->get<UnreleasedIvarMap>().isEmpty()) |
| return State; |
| |
| // Don't treat calls to '[super dealloc]' as escaping for the purposes |
| // of this checker. Because the checker diagnoses missing releases in the |
| // post-message handler for '[super dealloc], escaping here would cause |
| // the checker to never warn. |
| auto *OMC = dyn_cast_or_null<ObjCMethodCall>(Call); |
| if (OMC && isSuperDeallocMessage(*OMC)) |
| return State; |
| |
| for (const auto &Sym : Escaped) { |
| if (!Call || (Call && !Call->isInSystemHeader())) { |
| // If Sym is a symbol for an object with instance variables that |
| // must be released, remove these obligations when the object escapes |
| // unless via a call to a system function. System functions are |
| // very unlikely to release instance variables on objects passed to them, |
| // and are frequently called on 'self' in -dealloc (e.g., to remove |
| // observers) -- we want to avoid false negatives from escaping on |
| // them. |
| State = State->remove<UnreleasedIvarMap>(Sym); |
| } |
| |
| |
| SymbolRef InstanceSymbol = getInstanceSymbolFromIvarSymbol(Sym); |
| if (!InstanceSymbol) |
| continue; |
| |
| State = removeValueRequiringRelease(State, InstanceSymbol, Sym); |
| } |
| |
| return State; |
| } |
| |
| /// Report any unreleased instance variables for the current instance being |
| /// dealloced. |
| void ObjCDeallocChecker::diagnoseMissingReleases(CheckerContext &C) const { |
| ProgramStateRef State = C.getState(); |
| |
| SVal SelfVal; |
| if (!isInInstanceDealloc(C, SelfVal)) |
| return; |
| |
| const MemRegion *SelfRegion = SelfVal.castAs<loc::MemRegionVal>().getRegion(); |
| const LocationContext *LCtx = C.getLocationContext(); |
| |
| ExplodedNode *ErrNode = nullptr; |
| |
| SymbolRef SelfSym = SelfVal.getAsSymbol(); |
| if (!SelfSym) |
| return; |
| |
| const SymbolSet *OldUnreleased = State->get<UnreleasedIvarMap>(SelfSym); |
| if (!OldUnreleased) |
| return; |
| |
| SymbolSet NewUnreleased = *OldUnreleased; |
| SymbolSet::Factory &F = State->getStateManager().get_context<SymbolSet>(); |
| |
| ProgramStateRef InitialState = State; |
| |
| for (auto *IvarSymbol : *OldUnreleased) { |
| const TypedValueRegion *TVR = |
| cast<SymbolRegionValue>(IvarSymbol)->getRegion(); |
| const ObjCIvarRegion *IvarRegion = cast<ObjCIvarRegion>(TVR); |
| |
| // Don't warn if the ivar is not for this instance. |
| if (SelfRegion != IvarRegion->getSuperRegion()) |
| continue; |
| |
| const ObjCIvarDecl *IvarDecl = IvarRegion->getDecl(); |
| // Prevent an inlined call to -dealloc in a super class from warning |
| // about the values the subclass's -dealloc should release. |
| if (IvarDecl->getContainingInterface() != |
| cast<ObjCMethodDecl>(LCtx->getDecl())->getClassInterface()) |
| continue; |
| |
| // Prevents diagnosing multiple times for the same instance variable |
| // at, for example, both a return and at the end of the function. |
| NewUnreleased = F.remove(NewUnreleased, IvarSymbol); |
| |
| if (State->getStateManager() |
| .getConstraintManager() |
| .isNull(State, IvarSymbol) |
| .isConstrainedTrue()) { |
| continue; |
| } |
| |
| // A missing release manifests as a leak, so treat as a non-fatal error. |
| if (!ErrNode) |
| ErrNode = C.generateNonFatalErrorNode(); |
| // If we've already reached this node on another path, return without |
| // diagnosing. |
| if (!ErrNode) |
| return; |
| |
| std::string Buf; |
| llvm::raw_string_ostream OS(Buf); |
| |
| const ObjCInterfaceDecl *Interface = IvarDecl->getContainingInterface(); |
| // If the class is known to have a lifecycle with teardown that is |
| // separate from -dealloc, do not warn about missing releases. We |
| // suppress here (rather than not tracking for instance variables in |
| // such classes) because these classes are rare. |
| if (classHasSeparateTeardown(Interface)) |
| return; |
| |
| ObjCImplDecl *ImplDecl = Interface->getImplementation(); |
| |
| const ObjCPropertyImplDecl *PropImpl = |
| ImplDecl->FindPropertyImplIvarDecl(IvarDecl->getIdentifier()); |
| |
| const ObjCPropertyDecl *PropDecl = PropImpl->getPropertyDecl(); |
| |
| assert(PropDecl->getSetterKind() == ObjCPropertyDecl::Copy || |
| PropDecl->getSetterKind() == ObjCPropertyDecl::Retain); |
| |
| OS << "The '" << *IvarDecl << "' ivar in '" << *ImplDecl |
| << "' was "; |
| |
| if (PropDecl->getSetterKind() == ObjCPropertyDecl::Retain) |
| OS << "retained"; |
| else |
| OS << "copied"; |
| |
| OS << " by a synthesized property but not released" |
| " before '[super dealloc]'"; |
| |
| auto BR = std::make_unique<PathSensitiveBugReport>(*MissingReleaseBugType, |
| OS.str(), ErrNode); |
| C.emitReport(std::move(BR)); |
| } |
| |
| if (NewUnreleased.isEmpty()) { |
| State = State->remove<UnreleasedIvarMap>(SelfSym); |
| } else { |
| State = State->set<UnreleasedIvarMap>(SelfSym, NewUnreleased); |
| } |
| |
| if (ErrNode) { |
| C.addTransition(State, ErrNode); |
| } else if (State != InitialState) { |
| C.addTransition(State); |
| } |
| |
| // Make sure that after checking in the top-most frame the list of |
| // tracked ivars is empty. This is intended to detect accidental leaks in |
| // the UnreleasedIvarMap program state. |
| assert(!LCtx->inTopFrame() || State->get<UnreleasedIvarMap>().isEmpty()); |
| } |
| |
| /// Given a symbol, determine whether the symbol refers to an ivar on |
| /// the top-most deallocating instance. If so, find the property for that |
| /// ivar, if one exists. Otherwise return null. |
| const ObjCPropertyImplDecl * |
| ObjCDeallocChecker::findPropertyOnDeallocatingInstance( |
| SymbolRef IvarSym, CheckerContext &C) const { |
| SVal DeallocedInstance; |
| if (!isInInstanceDealloc(C, DeallocedInstance)) |
| return nullptr; |
| |
| // Try to get the region from which the ivar value was loaded. |
| auto *IvarRegion = getIvarRegionForIvarSymbol(IvarSym); |
| if (!IvarRegion) |
| return nullptr; |
| |
| // Don't try to find the property if the ivar was not loaded from the |
| // given instance. |
| if (DeallocedInstance.castAs<loc::MemRegionVal>().getRegion() != |
| IvarRegion->getSuperRegion()) |
| return nullptr; |
| |
| const LocationContext *LCtx = C.getLocationContext(); |
| const ObjCIvarDecl *IvarDecl = IvarRegion->getDecl(); |
| |
| const ObjCImplDecl *Container = getContainingObjCImpl(LCtx); |
| const ObjCPropertyImplDecl *PropImpl = |
| Container->FindPropertyImplIvarDecl(IvarDecl->getIdentifier()); |
| return PropImpl; |
| } |
| |
| /// Emits a warning if the current context is -dealloc and ReleasedValue |
| /// must not be directly released in a -dealloc. Returns true if a diagnostic |
| /// was emitted. |
| bool ObjCDeallocChecker::diagnoseExtraRelease(SymbolRef ReleasedValue, |
| const ObjCMethodCall &M, |
| CheckerContext &C) const { |
| // Try to get the region from which the released value was loaded. |
| // Note that, unlike diagnosing for missing releases, here we don't track |
| // values that must not be released in the state. This is because even if |
| // these values escape, it is still an error under the rules of MRR to |
| // release them in -dealloc. |
| const ObjCPropertyImplDecl *PropImpl = |
| findPropertyOnDeallocatingInstance(ReleasedValue, C); |
| |
| if (!PropImpl) |
| return false; |
| |
| // If the ivar belongs to a property that must not be released directly |
| // in dealloc, emit a warning. |
| if (getDeallocReleaseRequirement(PropImpl) != |
| ReleaseRequirement::MustNotReleaseDirectly) { |
| return false; |
| } |
| |
| // If the property is readwrite but it shadows a read-only property in its |
| // external interface, treat the property a read-only. If the outside |
| // world cannot write to a property then the internal implementation is free |
| // to make its own convention about whether the value is stored retained |
| // or not. We look up the shadow here rather than in |
| // getDeallocReleaseRequirement() because doing so can be expensive. |
| const ObjCPropertyDecl *PropDecl = findShadowedPropertyDecl(PropImpl); |
| if (PropDecl) { |
| if (PropDecl->isReadOnly()) |
| return false; |
| } else { |
| PropDecl = PropImpl->getPropertyDecl(); |
| } |
| |
| ExplodedNode *ErrNode = C.generateNonFatalErrorNode(); |
| if (!ErrNode) |
| return false; |
| |
| std::string Buf; |
| llvm::raw_string_ostream OS(Buf); |
| |
| assert(PropDecl->getSetterKind() == ObjCPropertyDecl::Weak || |
| (PropDecl->getSetterKind() == ObjCPropertyDecl::Assign && |
| !PropDecl->isReadOnly()) || |
| isReleasedByCIFilterDealloc(PropImpl) |
| ); |
| |
| const ObjCImplDecl *Container = getContainingObjCImpl(C.getLocationContext()); |
| OS << "The '" << *PropImpl->getPropertyIvarDecl() |
| << "' ivar in '" << *Container; |
| |
| |
| if (isReleasedByCIFilterDealloc(PropImpl)) { |
| OS << "' will be released by '-[CIFilter dealloc]' but also released here"; |
| } else { |
| OS << "' was synthesized for "; |
| |
| if (PropDecl->getSetterKind() == ObjCPropertyDecl::Weak) |
| OS << "a weak"; |
| else |
| OS << "an assign, readwrite"; |
| |
| OS << " property but was released in 'dealloc'"; |
| } |
| |
| auto BR = std::make_unique<PathSensitiveBugReport>(*ExtraReleaseBugType, |
| OS.str(), ErrNode); |
| BR->addRange(M.getOriginExpr()->getSourceRange()); |
| |
| C.emitReport(std::move(BR)); |
| |
| return true; |
| } |
| |
| /// Emits a warning if the current context is -dealloc and DeallocedValue |
| /// must not be directly dealloced in a -dealloc. Returns true if a diagnostic |
| /// was emitted. |
| bool ObjCDeallocChecker::diagnoseMistakenDealloc(SymbolRef DeallocedValue, |
| const ObjCMethodCall &M, |
| CheckerContext &C) const { |
| // TODO: Apart from unknown/undefined receivers, this may happen when |
| // dealloc is called as a class method. Should we warn? |
| if (!DeallocedValue) |
| return false; |
| |
| // Find the property backing the instance variable that M |
| // is dealloc'ing. |
| const ObjCPropertyImplDecl *PropImpl = |
| findPropertyOnDeallocatingInstance(DeallocedValue, C); |
| if (!PropImpl) |
| return false; |
| |
| if (getDeallocReleaseRequirement(PropImpl) != |
| ReleaseRequirement::MustRelease) { |
| return false; |
| } |
| |
| ExplodedNode *ErrNode = C.generateErrorNode(); |
| if (!ErrNode) |
| return false; |
| |
| std::string Buf; |
| llvm::raw_string_ostream OS(Buf); |
| |
| OS << "'" << *PropImpl->getPropertyIvarDecl() |
| << "' should be released rather than deallocated"; |
| |
| auto BR = std::make_unique<PathSensitiveBugReport>(*MistakenDeallocBugType, |
| OS.str(), ErrNode); |
| BR->addRange(M.getOriginExpr()->getSourceRange()); |
| |
| C.emitReport(std::move(BR)); |
| |
| return true; |
| } |
| |
| ObjCDeallocChecker::ObjCDeallocChecker() |
| : NSObjectII(nullptr), SenTestCaseII(nullptr), XCTestCaseII(nullptr), |
| CIFilterII(nullptr) { |
| |
| MissingReleaseBugType.reset( |
| new BugType(this, "Missing ivar release (leak)", |
| categories::MemoryRefCount)); |
| |
| ExtraReleaseBugType.reset( |
| new BugType(this, "Extra ivar release", |
| categories::MemoryRefCount)); |
| |
| MistakenDeallocBugType.reset( |
| new BugType(this, "Mistaken dealloc", |
| categories::MemoryRefCount)); |
| } |
| |
| void ObjCDeallocChecker::initIdentifierInfoAndSelectors( |
| ASTContext &Ctx) const { |
| if (NSObjectII) |
| return; |
| |
| NSObjectII = &Ctx.Idents.get("NSObject"); |
| SenTestCaseII = &Ctx.Idents.get("SenTestCase"); |
| XCTestCaseII = &Ctx.Idents.get("XCTestCase"); |
| Block_releaseII = &Ctx.Idents.get("_Block_release"); |
| CIFilterII = &Ctx.Idents.get("CIFilter"); |
| |
| IdentifierInfo *DeallocII = &Ctx.Idents.get("dealloc"); |
| IdentifierInfo *ReleaseII = &Ctx.Idents.get("release"); |
| DeallocSel = Ctx.Selectors.getSelector(0, &DeallocII); |
| ReleaseSel = Ctx.Selectors.getSelector(0, &ReleaseII); |
| } |
| |
| /// Returns true if M is a call to '[super dealloc]'. |
| bool ObjCDeallocChecker::isSuperDeallocMessage( |
| const ObjCMethodCall &M) const { |
| if (M.getOriginExpr()->getReceiverKind() != ObjCMessageExpr::SuperInstance) |
| return false; |
| |
| return M.getSelector() == DeallocSel; |
| } |
| |
| /// Returns the ObjCImplDecl containing the method declaration in LCtx. |
| const ObjCImplDecl * |
| ObjCDeallocChecker::getContainingObjCImpl(const LocationContext *LCtx) const { |
| auto *MD = cast<ObjCMethodDecl>(LCtx->getDecl()); |
| return cast<ObjCImplDecl>(MD->getDeclContext()); |
| } |
| |
| /// Returns the property that shadowed by PropImpl if one exists and |
| /// nullptr otherwise. |
| const ObjCPropertyDecl *ObjCDeallocChecker::findShadowedPropertyDecl( |
| const ObjCPropertyImplDecl *PropImpl) const { |
| const ObjCPropertyDecl *PropDecl = PropImpl->getPropertyDecl(); |
| |
| // Only readwrite properties can shadow. |
| if (PropDecl->isReadOnly()) |
| return nullptr; |
| |
| auto *CatDecl = dyn_cast<ObjCCategoryDecl>(PropDecl->getDeclContext()); |
| |
| // Only class extensions can contain shadowing properties. |
| if (!CatDecl || !CatDecl->IsClassExtension()) |
| return nullptr; |
| |
| IdentifierInfo *ID = PropDecl->getIdentifier(); |
| DeclContext::lookup_result R = CatDecl->getClassInterface()->lookup(ID); |
| for (DeclContext::lookup_iterator I = R.begin(), E = R.end(); I != E; ++I) { |
| auto *ShadowedPropDecl = dyn_cast<ObjCPropertyDecl>(*I); |
| if (!ShadowedPropDecl) |
| continue; |
| |
| if (ShadowedPropDecl->isInstanceProperty()) { |
| assert(ShadowedPropDecl->isReadOnly()); |
| return ShadowedPropDecl; |
| } |
| } |
| |
| return nullptr; |
| } |
| |
| /// Add a transition noting the release of the given value. |
| void ObjCDeallocChecker::transitionToReleaseValue(CheckerContext &C, |
| SymbolRef Value) const { |
| assert(Value); |
| SymbolRef InstanceSym = getInstanceSymbolFromIvarSymbol(Value); |
| if (!InstanceSym) |
| return; |
| ProgramStateRef InitialState = C.getState(); |
| |
| ProgramStateRef ReleasedState = |
| removeValueRequiringRelease(InitialState, InstanceSym, Value); |
| |
| if (ReleasedState != InitialState) { |
| C.addTransition(ReleasedState); |
| } |
| } |
| |
| /// Remove the Value requiring a release from the tracked set for |
| /// Instance and return the resultant state. |
| ProgramStateRef ObjCDeallocChecker::removeValueRequiringRelease( |
| ProgramStateRef State, SymbolRef Instance, SymbolRef Value) const { |
| assert(Instance); |
| assert(Value); |
| const ObjCIvarRegion *RemovedRegion = getIvarRegionForIvarSymbol(Value); |
| if (!RemovedRegion) |
| return State; |
| |
| const SymbolSet *Unreleased = State->get<UnreleasedIvarMap>(Instance); |
| if (!Unreleased) |
| return State; |
| |
| // Mark the value as no longer requiring a release. |
| SymbolSet::Factory &F = State->getStateManager().get_context<SymbolSet>(); |
| SymbolSet NewUnreleased = *Unreleased; |
| for (auto &Sym : *Unreleased) { |
| const ObjCIvarRegion *UnreleasedRegion = getIvarRegionForIvarSymbol(Sym); |
| assert(UnreleasedRegion); |
| if (RemovedRegion->getDecl() == UnreleasedRegion->getDecl()) { |
| NewUnreleased = F.remove(NewUnreleased, Sym); |
| } |
| } |
| |
| if (NewUnreleased.isEmpty()) { |
| return State->remove<UnreleasedIvarMap>(Instance); |
| } |
| |
| return State->set<UnreleasedIvarMap>(Instance, NewUnreleased); |
| } |
| |
| /// Determines whether the instance variable for \p PropImpl must or must not be |
| /// released in -dealloc or whether it cannot be determined. |
| ReleaseRequirement ObjCDeallocChecker::getDeallocReleaseRequirement( |
| const ObjCPropertyImplDecl *PropImpl) const { |
| const ObjCIvarDecl *IvarDecl; |
| const ObjCPropertyDecl *PropDecl; |
| if (!isSynthesizedRetainableProperty(PropImpl, &IvarDecl, &PropDecl)) |
| return ReleaseRequirement::Unknown; |
| |
| ObjCPropertyDecl::SetterKind SK = PropDecl->getSetterKind(); |
| |
| switch (SK) { |
| // Retain and copy setters retain/copy their values before storing and so |
| // the value in their instance variables must be released in -dealloc. |
| case ObjCPropertyDecl::Retain: |
| case ObjCPropertyDecl::Copy: |
| if (isReleasedByCIFilterDealloc(PropImpl)) |
| return ReleaseRequirement::MustNotReleaseDirectly; |
| |
| if (isNibLoadedIvarWithoutRetain(PropImpl)) |
| return ReleaseRequirement::Unknown; |
| |
| return ReleaseRequirement::MustRelease; |
| |
| case ObjCPropertyDecl::Weak: |
| return ReleaseRequirement::MustNotReleaseDirectly; |
| |
| case ObjCPropertyDecl::Assign: |
| // It is common for the ivars for read-only assign properties to |
| // always be stored retained, so their release requirement cannot be |
| // be determined. |
| if (PropDecl->isReadOnly()) |
| return ReleaseRequirement::Unknown; |
| |
| return ReleaseRequirement::MustNotReleaseDirectly; |
| } |
| llvm_unreachable("Unrecognized setter kind"); |
| } |
| |
| /// Returns the released value if M is a call a setter that releases |
| /// and nils out its underlying instance variable. |
| SymbolRef |
| ObjCDeallocChecker::getValueReleasedByNillingOut(const ObjCMethodCall &M, |
| CheckerContext &C) const { |
| SVal ReceiverVal = M.getReceiverSVal(); |
| if (!ReceiverVal.isValid()) |
| return nullptr; |
| |
| if (M.getNumArgs() == 0) |
| return nullptr; |
| |
| if (!M.getArgExpr(0)->getType()->isObjCRetainableType()) |
| return nullptr; |
| |
| // Is the first argument nil? |
| SVal Arg = M.getArgSVal(0); |
| ProgramStateRef notNilState, nilState; |
| std::tie(notNilState, nilState) = |
| M.getState()->assume(Arg.castAs<DefinedOrUnknownSVal>()); |
| if (!(nilState && !notNilState)) |
| return nullptr; |
| |
| const ObjCPropertyDecl *Prop = M.getAccessedProperty(); |
| if (!Prop) |
| return nullptr; |
| |
| ObjCIvarDecl *PropIvarDecl = Prop->getPropertyIvarDecl(); |
| if (!PropIvarDecl) |
| return nullptr; |
| |
| ProgramStateRef State = C.getState(); |
| |
| SVal LVal = State->getLValue(PropIvarDecl, ReceiverVal); |
| Optional<Loc> LValLoc = LVal.getAs<Loc>(); |
| if (!LValLoc) |
| return nullptr; |
| |
| SVal CurrentValInIvar = State->getSVal(LValLoc.getValue()); |
| return CurrentValInIvar.getAsSymbol(); |
| } |
| |
| /// Returns true if the current context is a call to -dealloc and false |
| /// otherwise. If true, it also sets SelfValOut to the value of |
| /// 'self'. |
| bool ObjCDeallocChecker::isInInstanceDealloc(const CheckerContext &C, |
| SVal &SelfValOut) const { |
| return isInInstanceDealloc(C, C.getLocationContext(), SelfValOut); |
| } |
| |
| /// Returns true if LCtx is a call to -dealloc and false |
| /// otherwise. If true, it also sets SelfValOut to the value of |
| /// 'self'. |
| bool ObjCDeallocChecker::isInInstanceDealloc(const CheckerContext &C, |
| const LocationContext *LCtx, |
| SVal &SelfValOut) const { |
| auto *MD = dyn_cast<ObjCMethodDecl>(LCtx->getDecl()); |
| if (!MD || !MD->isInstanceMethod() || MD->getSelector() != DeallocSel) |
| return false; |
| |
| const ImplicitParamDecl *SelfDecl = LCtx->getSelfDecl(); |
| assert(SelfDecl && "No self in -dealloc?"); |
| |
| ProgramStateRef State = C.getState(); |
| SelfValOut = State->getSVal(State->getRegion(SelfDecl, LCtx)); |
| return true; |
| } |
| |
| /// Returns true if there is a call to -dealloc anywhere on the stack and false |
| /// otherwise. If true, it also sets InstanceValOut to the value of |
| /// 'self' in the frame for -dealloc. |
| bool ObjCDeallocChecker::instanceDeallocIsOnStack(const CheckerContext &C, |
| SVal &InstanceValOut) const { |
| const LocationContext *LCtx = C.getLocationContext(); |
| |
| while (LCtx) { |
| if (isInInstanceDealloc(C, LCtx, InstanceValOut)) |
| return true; |
| |
| LCtx = LCtx->getParent(); |
| } |
| |
| return false; |
| } |
| |
| /// Returns true if the ID is a class in which which is known to have |
| /// a separate teardown lifecycle. In this case, -dealloc warnings |
| /// about missing releases should be suppressed. |
| bool ObjCDeallocChecker::classHasSeparateTeardown( |
| const ObjCInterfaceDecl *ID) const { |
| // Suppress if the class is not a subclass of NSObject. |
| for ( ; ID ; ID = ID->getSuperClass()) { |
| IdentifierInfo *II = ID->getIdentifier(); |
| |
| if (II == NSObjectII) |
| return false; |
| |
| // FIXME: For now, ignore classes that subclass SenTestCase and XCTestCase, |
| // as these don't need to implement -dealloc. They implement tear down in |
| // another way, which we should try and catch later. |
| // http://llvm.org/bugs/show_bug.cgi?id=3187 |
| if (II == XCTestCaseII || II == SenTestCaseII) |
| return true; |
| } |
| |
| return true; |
| } |
| |
| /// The -dealloc method in CIFilter highly unusual in that is will release |
| /// instance variables belonging to its *subclasses* if the variable name |
| /// starts with "input" or backs a property whose name starts with "input". |
| /// Subclasses should not release these ivars in their own -dealloc method -- |
| /// doing so could result in an over release. |
| /// |
| /// This method returns true if the property will be released by |
| /// -[CIFilter dealloc]. |
| bool ObjCDeallocChecker::isReleasedByCIFilterDealloc( |
| const ObjCPropertyImplDecl *PropImpl) const { |
| assert(PropImpl->getPropertyIvarDecl()); |
| StringRef PropName = PropImpl->getPropertyDecl()->getName(); |
| StringRef IvarName = PropImpl->getPropertyIvarDecl()->getName(); |
| |
| const char *ReleasePrefix = "input"; |
| if (!(PropName.startswith(ReleasePrefix) || |
| IvarName.startswith(ReleasePrefix))) { |
| return false; |
| } |
| |
| const ObjCInterfaceDecl *ID = |
| PropImpl->getPropertyIvarDecl()->getContainingInterface(); |
| for ( ; ID ; ID = ID->getSuperClass()) { |
| IdentifierInfo *II = ID->getIdentifier(); |
| if (II == CIFilterII) |
| return true; |
| } |
| |
| return false; |
| } |
| |
| /// Returns whether the ivar backing the property is an IBOutlet that |
| /// has its value set by nib loading code without retaining the value. |
| /// |
| /// On macOS, if there is no setter, the nib-loading code sets the ivar |
| /// directly, without retaining the value, |
| /// |
| /// On iOS and its derivatives, the nib-loading code will call |
| /// -setValue:forKey:, which retains the value before directly setting the ivar. |
| bool ObjCDeallocChecker::isNibLoadedIvarWithoutRetain( |
| const ObjCPropertyImplDecl *PropImpl) const { |
| const ObjCIvarDecl *IvarDecl = PropImpl->getPropertyIvarDecl(); |
| if (!IvarDecl->hasAttr<IBOutletAttr>()) |
| return false; |
| |
| const llvm::Triple &Target = |
| IvarDecl->getASTContext().getTargetInfo().getTriple(); |
| |
| if (!Target.isMacOSX()) |
| return false; |
| |
| if (PropImpl->getPropertyDecl()->getSetterMethodDecl()) |
| return false; |
| |
| return true; |
| } |
| |
| void ento::registerObjCDeallocChecker(CheckerManager &Mgr) { |
| Mgr.registerChecker<ObjCDeallocChecker>(); |
| } |
| |
| bool ento::shouldRegisterObjCDeallocChecker(const CheckerManager &mgr) { |
| // These checker only makes sense under MRR. |
| const LangOptions &LO = mgr.getLangOpts(); |
| return LO.getGC() != LangOptions::GCOnly && !LO.ObjCAutoRefCount; |
| } |