diff options
Diffstat (limited to 'contrib/llvm-project/clang/lib/StaticAnalyzer/Checkers/MallocChecker.cpp')
-rw-r--r-- | contrib/llvm-project/clang/lib/StaticAnalyzer/Checkers/MallocChecker.cpp | 457 |
1 files changed, 262 insertions, 195 deletions
diff --git a/contrib/llvm-project/clang/lib/StaticAnalyzer/Checkers/MallocChecker.cpp b/contrib/llvm-project/clang/lib/StaticAnalyzer/Checkers/MallocChecker.cpp index 79ab05f2c786..fe202c79ed62 100644 --- a/contrib/llvm-project/clang/lib/StaticAnalyzer/Checkers/MallocChecker.cpp +++ b/contrib/llvm-project/clang/lib/StaticAnalyzer/Checkers/MallocChecker.cpp @@ -46,6 +46,7 @@ #include "AllocationState.h" #include "InterCheckerAPI.h" +#include "NoOwnershipChangeVisitor.h" #include "clang/AST/Attr.h" #include "clang/AST/DeclCXX.h" #include "clang/AST/DeclTemplate.h" @@ -60,6 +61,7 @@ #include "clang/Basic/TargetInfo.h" #include "clang/Lex/Lexer.h" #include "clang/StaticAnalyzer/Checkers/BuiltinCheckerRegistration.h" +#include "clang/StaticAnalyzer/Checkers/Taint.h" #include "clang/StaticAnalyzer/Core/BugReporter/BugType.h" #include "clang/StaticAnalyzer/Core/BugReporter/CommonBugCategories.h" #include "clang/StaticAnalyzer/Core/Checker.h" @@ -78,13 +80,11 @@ #include "clang/StaticAnalyzer/Core/PathSensitive/SymbolManager.h" #include "llvm/ADT/STLExtras.h" #include "llvm/ADT/SetOperations.h" -#include "llvm/ADT/SmallString.h" #include "llvm/ADT/StringExtras.h" #include "llvm/Support/Casting.h" #include "llvm/Support/Compiler.h" #include "llvm/Support/ErrorHandling.h" #include "llvm/Support/raw_ostream.h" -#include <climits> #include <functional> #include <optional> #include <utility> @@ -322,6 +322,7 @@ public: CK_NewDeleteLeaksChecker, CK_MismatchedDeallocatorChecker, CK_InnerPointerChecker, + CK_TaintedAllocChecker, CK_NumCheckKinds }; @@ -365,6 +366,7 @@ private: mutable std::unique_ptr<BugType> BT_MismatchedDealloc; mutable std::unique_ptr<BugType> BT_OffsetFree[CK_NumCheckKinds]; mutable std::unique_ptr<BugType> BT_UseZerroAllocated[CK_NumCheckKinds]; + mutable std::unique_ptr<BugType> BT_TaintedAlloc; #define CHECK_FN(NAME) \ void NAME(const CallEvent &Call, CheckerContext &C) const; @@ -382,6 +384,8 @@ private: CHECK_FN(checkGMemdup) CHECK_FN(checkGMallocN) CHECK_FN(checkGMallocN0) + CHECK_FN(preGetdelim) + CHECK_FN(checkGetdelim) CHECK_FN(checkReallocN) CHECK_FN(checkOwnershipAttr) @@ -391,57 +395,82 @@ private: using CheckFn = std::function<void(const MallocChecker *, const CallEvent &Call, CheckerContext &C)>; + const CallDescriptionMap<CheckFn> PreFnMap{ + // NOTE: the following CallDescription also matches the C++ standard + // library function std::getline(); the callback will filter it out. + {{CDM::CLibrary, {"getline"}, 3}, &MallocChecker::preGetdelim}, + {{CDM::CLibrary, {"getdelim"}, 4}, &MallocChecker::preGetdelim}, + }; + const CallDescriptionMap<CheckFn> FreeingMemFnMap{ - {{{"free"}, 1}, &MallocChecker::checkFree}, - {{{"if_freenameindex"}, 1}, &MallocChecker::checkIfFreeNameIndex}, - {{{"kfree"}, 1}, &MallocChecker::checkFree}, - {{{"g_free"}, 1}, &MallocChecker::checkFree}, + {{CDM::CLibrary, {"free"}, 1}, &MallocChecker::checkFree}, + {{CDM::CLibrary, {"if_freenameindex"}, 1}, + &MallocChecker::checkIfFreeNameIndex}, + {{CDM::CLibrary, {"kfree"}, 1}, &MallocChecker::checkFree}, + {{CDM::CLibrary, {"g_free"}, 1}, &MallocChecker::checkFree}, }; bool isFreeingCall(const CallEvent &Call) const; static bool isFreeingOwnershipAttrCall(const FunctionDecl *Func); - friend class NoOwnershipChangeVisitor; + friend class NoMemOwnershipChangeVisitor; CallDescriptionMap<CheckFn> AllocatingMemFnMap{ - {{{"alloca"}, 1}, &MallocChecker::checkAlloca}, - {{{"_alloca"}, 1}, &MallocChecker::checkAlloca}, - {{{"malloc"}, 1}, &MallocChecker::checkBasicAlloc}, - {{{"malloc"}, 3}, &MallocChecker::checkKernelMalloc}, - {{{"calloc"}, 2}, &MallocChecker::checkCalloc}, - {{{"valloc"}, 1}, &MallocChecker::checkBasicAlloc}, - {{CDF_MaybeBuiltin, {"strndup"}, 2}, &MallocChecker::checkStrdup}, - {{CDF_MaybeBuiltin, {"strdup"}, 1}, &MallocChecker::checkStrdup}, - {{{"_strdup"}, 1}, &MallocChecker::checkStrdup}, - {{{"kmalloc"}, 2}, &MallocChecker::checkKernelMalloc}, - {{{"if_nameindex"}, 1}, &MallocChecker::checkIfNameIndex}, - {{CDF_MaybeBuiltin, {"wcsdup"}, 1}, &MallocChecker::checkStrdup}, - {{CDF_MaybeBuiltin, {"_wcsdup"}, 1}, &MallocChecker::checkStrdup}, - {{{"g_malloc"}, 1}, &MallocChecker::checkBasicAlloc}, - {{{"g_malloc0"}, 1}, &MallocChecker::checkGMalloc0}, - {{{"g_try_malloc"}, 1}, &MallocChecker::checkBasicAlloc}, - {{{"g_try_malloc0"}, 1}, &MallocChecker::checkGMalloc0}, - {{{"g_memdup"}, 2}, &MallocChecker::checkGMemdup}, - {{{"g_malloc_n"}, 2}, &MallocChecker::checkGMallocN}, - {{{"g_malloc0_n"}, 2}, &MallocChecker::checkGMallocN0}, - {{{"g_try_malloc_n"}, 2}, &MallocChecker::checkGMallocN}, - {{{"g_try_malloc0_n"}, 2}, &MallocChecker::checkGMallocN0}, + {{CDM::CLibrary, {"alloca"}, 1}, &MallocChecker::checkAlloca}, + {{CDM::CLibrary, {"_alloca"}, 1}, &MallocChecker::checkAlloca}, + // The line for "alloca" also covers "__builtin_alloca", but the + // _with_align variant must be listed separately because it takes an + // extra argument: + {{CDM::CLibrary, {"__builtin_alloca_with_align"}, 2}, + &MallocChecker::checkAlloca}, + {{CDM::CLibrary, {"malloc"}, 1}, &MallocChecker::checkBasicAlloc}, + {{CDM::CLibrary, {"malloc"}, 3}, &MallocChecker::checkKernelMalloc}, + {{CDM::CLibrary, {"calloc"}, 2}, &MallocChecker::checkCalloc}, + {{CDM::CLibrary, {"valloc"}, 1}, &MallocChecker::checkBasicAlloc}, + {{CDM::CLibrary, {"strndup"}, 2}, &MallocChecker::checkStrdup}, + {{CDM::CLibrary, {"strdup"}, 1}, &MallocChecker::checkStrdup}, + {{CDM::CLibrary, {"_strdup"}, 1}, &MallocChecker::checkStrdup}, + {{CDM::CLibrary, {"kmalloc"}, 2}, &MallocChecker::checkKernelMalloc}, + {{CDM::CLibrary, {"if_nameindex"}, 1}, &MallocChecker::checkIfNameIndex}, + {{CDM::CLibrary, {"wcsdup"}, 1}, &MallocChecker::checkStrdup}, + {{CDM::CLibrary, {"_wcsdup"}, 1}, &MallocChecker::checkStrdup}, + {{CDM::CLibrary, {"g_malloc"}, 1}, &MallocChecker::checkBasicAlloc}, + {{CDM::CLibrary, {"g_malloc0"}, 1}, &MallocChecker::checkGMalloc0}, + {{CDM::CLibrary, {"g_try_malloc"}, 1}, &MallocChecker::checkBasicAlloc}, + {{CDM::CLibrary, {"g_try_malloc0"}, 1}, &MallocChecker::checkGMalloc0}, + {{CDM::CLibrary, {"g_memdup"}, 2}, &MallocChecker::checkGMemdup}, + {{CDM::CLibrary, {"g_malloc_n"}, 2}, &MallocChecker::checkGMallocN}, + {{CDM::CLibrary, {"g_malloc0_n"}, 2}, &MallocChecker::checkGMallocN0}, + {{CDM::CLibrary, {"g_try_malloc_n"}, 2}, &MallocChecker::checkGMallocN}, + {{CDM::CLibrary, {"g_try_malloc0_n"}, 2}, &MallocChecker::checkGMallocN0}, }; CallDescriptionMap<CheckFn> ReallocatingMemFnMap{ - {{{"realloc"}, 2}, + {{CDM::CLibrary, {"realloc"}, 2}, std::bind(&MallocChecker::checkRealloc, _1, _2, _3, false)}, - {{{"reallocf"}, 2}, + {{CDM::CLibrary, {"reallocf"}, 2}, std::bind(&MallocChecker::checkRealloc, _1, _2, _3, true)}, - {{{"g_realloc"}, 2}, + {{CDM::CLibrary, {"g_realloc"}, 2}, std::bind(&MallocChecker::checkRealloc, _1, _2, _3, false)}, - {{{"g_try_realloc"}, 2}, + {{CDM::CLibrary, {"g_try_realloc"}, 2}, std::bind(&MallocChecker::checkRealloc, _1, _2, _3, false)}, - {{{"g_realloc_n"}, 3}, &MallocChecker::checkReallocN}, - {{{"g_try_realloc_n"}, 3}, &MallocChecker::checkReallocN}, + {{CDM::CLibrary, {"g_realloc_n"}, 3}, &MallocChecker::checkReallocN}, + {{CDM::CLibrary, {"g_try_realloc_n"}, 3}, &MallocChecker::checkReallocN}, + + // NOTE: the following CallDescription also matches the C++ standard + // library function std::getline(); the callback will filter it out. + {{CDM::CLibrary, {"getline"}, 3}, &MallocChecker::checkGetdelim}, + {{CDM::CLibrary, {"getdelim"}, 4}, &MallocChecker::checkGetdelim}, }; bool isMemCall(const CallEvent &Call) const; + void reportTaintBug(StringRef Msg, ProgramStateRef State, CheckerContext &C, + llvm::ArrayRef<SymbolRef> TaintedSyms, + AllocationFamily Family) const; + + void checkTaintedness(CheckerContext &C, const CallEvent &Call, + const SVal SizeSVal, ProgramStateRef State, + AllocationFamily Family) const; // TODO: Remove mutable by moving the initializtaion to the registry function. mutable std::optional<uint64_t> KernelZeroFlagVal; @@ -501,9 +530,9 @@ private: /// malloc leaves it undefined. /// \param [in] State The \c ProgramState right before allocation. /// \returns The ProgramState right after allocation. - [[nodiscard]] static ProgramStateRef + [[nodiscard]] ProgramStateRef MallocMemAux(CheckerContext &C, const CallEvent &Call, const Expr *SizeEx, - SVal Init, ProgramStateRef State, AllocationFamily Family); + SVal Init, ProgramStateRef State, AllocationFamily Family) const; /// Models memory allocation. /// @@ -514,9 +543,10 @@ private: /// malloc leaves it undefined. /// \param [in] State The \c ProgramState right before allocation. /// \returns The ProgramState right after allocation. - [[nodiscard]] static ProgramStateRef - MallocMemAux(CheckerContext &C, const CallEvent &Call, SVal Size, SVal Init, - ProgramStateRef State, AllocationFamily Family); + [[nodiscard]] ProgramStateRef MallocMemAux(CheckerContext &C, + const CallEvent &Call, SVal Size, + SVal Init, ProgramStateRef State, + AllocationFamily Family) const; // Check if this malloc() for special flags. At present that means M_ZERO or // __GFP_ZERO (in which case, treat it like calloc). @@ -588,11 +618,14 @@ private: /// } /// \param [in] ReturnsNullOnFailure Whether the memory deallocation function /// we're modeling returns with Null on failure. + /// \param [in] ArgValOpt Optional value to use for the argument instead of + /// the one obtained from ArgExpr. /// \returns The ProgramState right after deallocation. [[nodiscard]] ProgramStateRef FreeMemAux(CheckerContext &C, const Expr *ArgExpr, const CallEvent &Call, ProgramStateRef State, bool Hold, bool &IsKnownToBeAllocated, - AllocationFamily Family, bool ReturnsNullOnFailure = false) const; + AllocationFamily Family, bool ReturnsNullOnFailure = false, + std::optional<SVal> ArgValOpt = {}) const; // TODO: Needs some refactoring, as all other deallocation modeling // functions are suffering from out parameters and messy code due to how @@ -626,8 +659,9 @@ private: /// \param [in] Call The expression that reallocated memory /// \param [in] State The \c ProgramState right before reallocation. /// \returns The ProgramState right after allocation. - [[nodiscard]] static ProgramStateRef - CallocMem(CheckerContext &C, const CallEvent &Call, ProgramStateRef State); + [[nodiscard]] ProgramStateRef CallocMem(CheckerContext &C, + const CallEvent &Call, + ProgramStateRef State) const; /// See if deallocation happens in a suspicious context. If so, escape the /// pointers that otherwise would have been deallocated and return true. @@ -730,61 +764,8 @@ private: //===----------------------------------------------------------------------===// namespace { -class NoOwnershipChangeVisitor final : public NoStateChangeFuncVisitor { - // The symbol whose (lack of) ownership change we are interested in. - SymbolRef Sym; - const MallocChecker &Checker; - using OwnerSet = llvm::SmallPtrSet<const MemRegion *, 8>; - - // Collect which entities point to the allocated memory, and could be - // responsible for deallocating it. - class OwnershipBindingsHandler : public StoreManager::BindingsHandler { - SymbolRef Sym; - OwnerSet &Owners; - - public: - OwnershipBindingsHandler(SymbolRef Sym, OwnerSet &Owners) - : Sym(Sym), Owners(Owners) {} - - bool HandleBinding(StoreManager &SMgr, Store Store, const MemRegion *Region, - SVal Val) override { - if (Val.getAsSymbol() == Sym) - Owners.insert(Region); - return true; - } - - LLVM_DUMP_METHOD void dump() const { dumpToStream(llvm::errs()); } - LLVM_DUMP_METHOD void dumpToStream(llvm::raw_ostream &out) const { - out << "Owners: {\n"; - for (const MemRegion *Owner : Owners) { - out << " "; - Owner->dumpToStream(out); - out << ",\n"; - } - out << "}\n"; - } - }; - +class NoMemOwnershipChangeVisitor final : public NoOwnershipChangeVisitor { protected: - OwnerSet getOwnersAtNode(const ExplodedNode *N) { - OwnerSet Ret; - - ProgramStateRef State = N->getState(); - OwnershipBindingsHandler Handler{Sym, Ret}; - State->getStateManager().getStoreManager().iterBindings(State->getStore(), - Handler); - return Ret; - } - - LLVM_DUMP_METHOD static std::string - getFunctionName(const ExplodedNode *CallEnterN) { - if (const CallExpr *CE = llvm::dyn_cast_or_null<CallExpr>( - CallEnterN->getLocationAs<CallEnter>()->getCallExpr())) - if (const FunctionDecl *FD = CE->getDirectCallee()) - return FD->getQualifiedNameAsString(); - return ""; - } - /// Syntactically checks whether the callee is a deallocating function. Since /// we have no path-sensitive information on this call (we would need a /// CallEvent instead of a CallExpr for that), its possible that a @@ -793,8 +774,9 @@ protected: /// See namespace `memory_passed_to_fn_call_free_through_fn_ptr` in /// clang/test/Analysis/NewDeleteLeaks.cpp. bool isFreeingCallAsWritten(const CallExpr &Call) const { - if (Checker.FreeingMemFnMap.lookupAsWritten(Call) || - Checker.ReallocatingMemFnMap.lookupAsWritten(Call)) + const auto *MallocChk = static_cast<const MallocChecker *>(&Checker); + if (MallocChk->FreeingMemFnMap.lookupAsWritten(Call) || + MallocChk->ReallocatingMemFnMap.lookupAsWritten(Call)) return true; if (const auto *Func = @@ -804,23 +786,21 @@ protected: return false; } + bool hasResourceStateChanged(ProgramStateRef CallEnterState, + ProgramStateRef CallExitEndState) final { + return CallEnterState->get<RegionState>(Sym) != + CallExitEndState->get<RegionState>(Sym); + } + /// Heuristically guess whether the callee intended to free memory. This is /// done syntactically, because we are trying to argue about alternative /// paths of execution, and as a consequence we don't have path-sensitive /// information. - bool doesFnIntendToHandleOwnership(const Decl *Callee, ASTContext &ACtx) { + bool doesFnIntendToHandleOwnership(const Decl *Callee, + ASTContext &ACtx) final { using namespace clang::ast_matchers; const FunctionDecl *FD = dyn_cast<FunctionDecl>(Callee); - // Given that the stack frame was entered, the body should always be - // theoretically obtainable. In case of body farms, the synthesized body - // is not attached to declaration, thus triggering the '!FD->hasBody()' - // branch. That said, would a synthesized body ever intend to handle - // ownership? As of today they don't. And if they did, how would we - // put notes inside it, given that it doesn't match any source locations? - if (!FD || !FD->hasBody()) - return false; - auto Matches = match(findAll(stmt(anyOf(cxxDeleteExpr().bind("delete"), callExpr().bind("call")))), *FD->getBody(), ACtx); @@ -838,30 +818,7 @@ protected: return false; } - bool wasModifiedInFunction(const ExplodedNode *CallEnterN, - const ExplodedNode *CallExitEndN) override { - if (!doesFnIntendToHandleOwnership( - CallExitEndN->getFirstPred()->getLocationContext()->getDecl(), - CallExitEndN->getState()->getAnalysisManager().getASTContext())) - return true; - - if (CallEnterN->getState()->get<RegionState>(Sym) != - CallExitEndN->getState()->get<RegionState>(Sym)) - return true; - - OwnerSet CurrOwners = getOwnersAtNode(CallEnterN); - OwnerSet ExitOwners = getOwnersAtNode(CallExitEndN); - - // Owners in the current set may be purged from the analyzer later on. - // If a variable is dead (is not referenced directly or indirectly after - // some point), it will be removed from the Store before the end of its - // actual lifetime. - // This means that if the ownership status didn't change, CurrOwners - // must be a superset of, but not necessarily equal to ExitOwners. - return !llvm::set_is_subset(ExitOwners, CurrOwners); - } - - static PathDiagnosticPieceRef emitNote(const ExplodedNode *N) { + PathDiagnosticPieceRef emitNote(const ExplodedNode *N) final { PathDiagnosticLocation L = PathDiagnosticLocation::create( N->getLocation(), N->getState()->getStateManager().getContext().getSourceManager()); @@ -870,42 +827,9 @@ protected: "later deallocation"); } - PathDiagnosticPieceRef - maybeEmitNoteForObjCSelf(PathSensitiveBugReport &R, - const ObjCMethodCall &Call, - const ExplodedNode *N) override { - // TODO: Implement. - return nullptr; - } - - PathDiagnosticPieceRef - maybeEmitNoteForCXXThis(PathSensitiveBugReport &R, - const CXXConstructorCall &Call, - const ExplodedNode *N) override { - // TODO: Implement. - return nullptr; - } - - PathDiagnosticPieceRef - maybeEmitNoteForParameters(PathSensitiveBugReport &R, const CallEvent &Call, - const ExplodedNode *N) override { - // TODO: Factor the logic of "what constitutes as an entity being passed - // into a function call" out by reusing the code in - // NoStoreFuncVisitor::maybeEmitNoteForParameters, maybe by incorporating - // the printing technology in UninitializedObject's FieldChainInfo. - ArrayRef<ParmVarDecl *> Parameters = Call.parameters(); - for (unsigned I = 0; I < Call.getNumArgs() && I < Parameters.size(); ++I) { - SVal V = Call.getArgSVal(I); - if (V.getAsSymbol() == Sym) - return emitNote(N); - } - return nullptr; - } - public: - NoOwnershipChangeVisitor(SymbolRef Sym, const MallocChecker *Checker) - : NoStateChangeFuncVisitor(bugreporter::TrackingKind::Thorough), Sym(Sym), - Checker(*Checker) {} + NoMemOwnershipChangeVisitor(SymbolRef Sym, const MallocChecker *Checker) + : NoOwnershipChangeVisitor(Sym, Checker) {} void Profile(llvm::FoldingSetNodeID &ID) const override { static int Tag = 0; @@ -1242,9 +1166,6 @@ static bool isStandardRealloc(const CallEvent &Call) { assert(FD); ASTContext &AC = FD->getASTContext(); - if (isa<CXXMethodDecl>(FD)) - return false; - return FD->getDeclaredReturnType().getDesugaredType(AC) == AC.VoidPtrTy && FD->getParamDecl(0)->getType().getDesugaredType(AC) == AC.VoidPtrTy && FD->getParamDecl(1)->getType().getDesugaredType(AC) == @@ -1256,9 +1177,6 @@ static bool isGRealloc(const CallEvent &Call) { assert(FD); ASTContext &AC = FD->getASTContext(); - if (isa<CXXMethodDecl>(FD)) - return false; - return FD->getDeclaredReturnType().getDesugaredType(AC) == AC.VoidPtrTy && FD->getParamDecl(0)->getType().getDesugaredType(AC) == AC.VoidPtrTy && FD->getParamDecl(1)->getType().getDesugaredType(AC) == @@ -1267,14 +1185,14 @@ static bool isGRealloc(const CallEvent &Call) { void MallocChecker::checkRealloc(const CallEvent &Call, CheckerContext &C, bool ShouldFreeOnFail) const { - // HACK: CallDescription currently recognizes non-standard realloc functions - // as standard because it doesn't check the type, or wether its a non-method - // function. This should be solved by making CallDescription smarter. - // Mind that this came from a bug report, and all other functions suffer from - // this. - // https://bugs.llvm.org/show_bug.cgi?id=46253 + // Ignore calls to functions whose type does not match the expected type of + // either the standard realloc or g_realloc from GLib. + // FIXME: Should we perform this kind of checking consistently for each + // function? If yes, then perhaps extend the `CallDescription` interface to + // handle this. if (!isStandardRealloc(Call) && !isGRealloc(Call)) return; + ProgramStateRef State = C.getState(); State = ReallocMemAux(C, Call, ShouldFreeOnFail, State, AF_Malloc); State = ProcessZeroAllocCheck(Call, 1, State); @@ -1423,6 +1341,62 @@ void MallocChecker::checkGMallocN0(const CallEvent &Call, C.addTransition(State); } +static bool isFromStdNamespace(const CallEvent &Call) { + const Decl *FD = Call.getDecl(); + assert(FD && "a CallDescription cannot match a call without a Decl"); + return FD->isInStdNamespace(); +} + +void MallocChecker::preGetdelim(const CallEvent &Call, + CheckerContext &C) const { + // Discard calls to the C++ standard library function std::getline(), which + // is completely unrelated to the POSIX getline() that we're checking. + if (isFromStdNamespace(Call)) + return; + + ProgramStateRef State = C.getState(); + const auto LinePtr = getPointeeVal(Call.getArgSVal(0), State); + if (!LinePtr) + return; + + // FreeMemAux takes IsKnownToBeAllocated as an output parameter, and it will + // be true after the call if the symbol was registered by this checker. + // We do not need this value here, as FreeMemAux will take care + // of reporting any violation of the preconditions. + bool IsKnownToBeAllocated = false; + State = FreeMemAux(C, Call.getArgExpr(0), Call, State, false, + IsKnownToBeAllocated, AF_Malloc, false, LinePtr); + if (State) + C.addTransition(State); +} + +void MallocChecker::checkGetdelim(const CallEvent &Call, + CheckerContext &C) const { + // Discard calls to the C++ standard library function std::getline(), which + // is completely unrelated to the POSIX getline() that we're checking. + if (isFromStdNamespace(Call)) + return; + + ProgramStateRef State = C.getState(); + // Handle the post-conditions of getline and getdelim: + // Register the new conjured value as an allocated buffer. + const CallExpr *CE = dyn_cast_or_null<CallExpr>(Call.getOriginExpr()); + if (!CE) + return; + + SValBuilder &SVB = C.getSValBuilder(); + + const auto LinePtr = + getPointeeVal(Call.getArgSVal(0), State)->getAs<DefinedSVal>(); + const auto Size = + getPointeeVal(Call.getArgSVal(1), State)->getAs<DefinedSVal>(); + if (!LinePtr || !Size || !LinePtr->getAsRegion()) + return; + + State = setDynamicExtent(State, LinePtr->getAsRegion(), *Size, SVB); + C.addTransition(MallocUpdateRefState(C, CE, State, AF_Malloc, *LinePtr)); +} + void MallocChecker::checkReallocN(const CallEvent &Call, CheckerContext &C) const { ProgramStateRef State = C.getState(); @@ -1622,6 +1596,11 @@ MallocChecker::processNewAllocation(const CXXAllocatorCall &Call, // MallocUpdateRefState() instead of MallocMemAux() which breaks the // existing binding. SVal Target = Call.getObjectUnderConstruction(); + if (Call.getOriginExpr()->isArray()) { + if (auto SizeEx = NE->getArraySize()) + checkTaintedness(C, Call, C.getSVal(*SizeEx), State, AF_CXXNewArray); + } + State = MallocUpdateRefState(C, NE, State, Family, Target); State = ProcessZeroAllocCheck(Call, 0, State, Target); return State; @@ -1654,7 +1633,7 @@ static std::optional<bool> getFreeWhenDoneArg(const ObjCMethodCall &Call) { // FIXME: We should not rely on fully-constrained symbols being folded. for (unsigned i = 1; i < S.getNumArgs(); ++i) - if (S.getNameForSlot(i).equals("freeWhenDone")) + if (S.getNameForSlot(i) == "freeWhenDone") return !Call.getArgSVal(i).isZeroConstant(); return std::nullopt; @@ -1706,7 +1685,7 @@ ProgramStateRef MallocChecker::MallocMemAux(CheckerContext &C, const CallEvent &Call, const Expr *SizeEx, SVal Init, ProgramStateRef State, - AllocationFamily Family) { + AllocationFamily Family) const { if (!State) return nullptr; @@ -1714,10 +1693,66 @@ ProgramStateRef MallocChecker::MallocMemAux(CheckerContext &C, return MallocMemAux(C, Call, C.getSVal(SizeEx), Init, State, Family); } +void MallocChecker::reportTaintBug(StringRef Msg, ProgramStateRef State, + CheckerContext &C, + llvm::ArrayRef<SymbolRef> TaintedSyms, + AllocationFamily Family) const { + if (ExplodedNode *N = C.generateNonFatalErrorNode(State, this)) { + if (!BT_TaintedAlloc) + BT_TaintedAlloc.reset(new BugType(CheckNames[CK_TaintedAllocChecker], + "Tainted Memory Allocation", + categories::TaintedData)); + auto R = std::make_unique<PathSensitiveBugReport>(*BT_TaintedAlloc, Msg, N); + for (auto TaintedSym : TaintedSyms) { + R->markInteresting(TaintedSym); + } + C.emitReport(std::move(R)); + } +} + +void MallocChecker::checkTaintedness(CheckerContext &C, const CallEvent &Call, + const SVal SizeSVal, ProgramStateRef State, + AllocationFamily Family) const { + if (!ChecksEnabled[CK_TaintedAllocChecker]) + return; + std::vector<SymbolRef> TaintedSyms = + taint::getTaintedSymbols(State, SizeSVal); + if (TaintedSyms.empty()) + return; + + SValBuilder &SVB = C.getSValBuilder(); + QualType SizeTy = SVB.getContext().getSizeType(); + QualType CmpTy = SVB.getConditionType(); + // In case the symbol is tainted, we give a warning if the + // size is larger than SIZE_MAX/4 + BasicValueFactory &BVF = SVB.getBasicValueFactory(); + const llvm::APSInt MaxValInt = BVF.getMaxValue(SizeTy); + NonLoc MaxLength = + SVB.makeIntVal(MaxValInt / APSIntType(MaxValInt).getValue(4)); + std::optional<NonLoc> SizeNL = SizeSVal.getAs<NonLoc>(); + auto Cmp = SVB.evalBinOpNN(State, BO_GE, *SizeNL, MaxLength, CmpTy) + .getAs<DefinedOrUnknownSVal>(); + if (!Cmp) + return; + auto [StateTooLarge, StateNotTooLarge] = State->assume(*Cmp); + if (!StateTooLarge && StateNotTooLarge) { + // We can prove that size is not too large so there is no issue. + return; + } + + std::string Callee = "Memory allocation function"; + if (Call.getCalleeIdentifier()) + Callee = Call.getCalleeIdentifier()->getName().str(); + reportTaintBug( + Callee + " is called with a tainted (potentially attacker controlled) " + "value. Make sure the value is bound checked.", + State, C, TaintedSyms, Family); +} + ProgramStateRef MallocChecker::MallocMemAux(CheckerContext &C, const CallEvent &Call, SVal Size, SVal Init, ProgramStateRef State, - AllocationFamily Family) { + AllocationFamily Family) const { if (!State) return nullptr; @@ -1746,6 +1781,8 @@ ProgramStateRef MallocChecker::MallocMemAux(CheckerContext &C, if (Size.isUndef()) Size = UnknownVal(); + checkTaintedness(C, Call, Size, State, AF_Malloc); + // Set the region's extent. State = setDynamicExtent(State, RetVal.getAsRegion(), Size.castAs<DefinedOrUnknownSVal>(), SVB); @@ -1769,9 +1806,18 @@ static ProgramStateRef MallocUpdateRefState(CheckerContext &C, const Expr *E, return nullptr; SymbolRef Sym = RetVal->getAsLocSymbol(); + // This is a return value of a function that was not inlined, such as malloc() // or new(). We've checked that in the caller. Therefore, it must be a symbol. assert(Sym); + // FIXME: In theory this assertion should fail for `alloca()` calls (because + // `AllocaRegion`s are not symbolic); but in practice this does not happen. + // As the current code appears to work correctly, I'm not touching this issue + // now, but it would be good to investigate and clarify this. + // Also note that perhaps the special `AllocaRegion` should be replaced by + // `SymbolicRegion` (or turned into a subclass of `SymbolicRegion`) to enable + // proper tracking of memory allocated by `alloca()` -- and after that change + // this assertion would become valid again. // Set the symbol's state to Allocated. return State->set<RegionState>(Sym, RefState::getAllocated(Family, E)); @@ -1895,15 +1941,17 @@ static void printExpectedDeallocName(raw_ostream &os, AllocationFamily Family) { } } -ProgramStateRef MallocChecker::FreeMemAux( - CheckerContext &C, const Expr *ArgExpr, const CallEvent &Call, - ProgramStateRef State, bool Hold, bool &IsKnownToBeAllocated, - AllocationFamily Family, bool ReturnsNullOnFailure) const { +ProgramStateRef +MallocChecker::FreeMemAux(CheckerContext &C, const Expr *ArgExpr, + const CallEvent &Call, ProgramStateRef State, + bool Hold, bool &IsKnownToBeAllocated, + AllocationFamily Family, bool ReturnsNullOnFailure, + std::optional<SVal> ArgValOpt) const { if (!State) return nullptr; - SVal ArgVal = C.getSVal(ArgExpr); + SVal ArgVal = ArgValOpt.value_or(C.getSVal(ArgExpr)); if (!isa<DefinedOrUnknownSVal>(ArgVal)) return nullptr; DefinedOrUnknownSVal location = ArgVal.castAs<DefinedOrUnknownSVal>(); @@ -2673,7 +2721,7 @@ MallocChecker::ReallocMemAux(CheckerContext &C, const CallEvent &Call, ProgramStateRef MallocChecker::CallocMem(CheckerContext &C, const CallEvent &Call, - ProgramStateRef State) { + ProgramStateRef State) const { if (!State) return nullptr; @@ -2790,7 +2838,7 @@ void MallocChecker::HandleLeak(SymbolRef Sym, ExplodedNode *N, R->markInteresting(Sym); R->addVisitor<MallocBugVisitor>(Sym, true); if (ShouldRegisterNoOwnershipChangeVisitor) - R->addVisitor<NoOwnershipChangeVisitor>(Sym, this); + R->addVisitor<NoMemOwnershipChangeVisitor>(Sym, this); C.emitReport(std::move(R)); } @@ -2881,6 +2929,13 @@ void MallocChecker::checkPreCall(const CallEvent &Call, return; } + // We need to handle getline pre-conditions here before the pointed region + // gets invalidated by StreamChecker + if (const auto *PreFN = PreFnMap.lookup(Call)) { + (*PreFN)(this, Call, C); + return; + } + // We will check for double free in the post visit. if (const AnyFunctionCall *FC = dyn_cast<AnyFunctionCall>(&Call)) { const FunctionDecl *FD = FC->getDecl(); @@ -3160,7 +3215,7 @@ bool MallocChecker::mayFreeAnyEscapedMemoryOrIsModeledExplicitly( if (FirstSlot.starts_with("addPointer") || FirstSlot.starts_with("insertPointer") || FirstSlot.starts_with("replacePointer") || - FirstSlot.equals("valueWithPointer")) { + FirstSlot == "valueWithPointer") { return true; } @@ -3356,7 +3411,7 @@ static bool isReferenceCountingPointerDestructor(const CXXDestructorDecl *DD) { if (N.contains_insensitive("ptr") || N.contains_insensitive("pointer")) { if (N.contains_insensitive("ref") || N.contains_insensitive("cnt") || N.contains_insensitive("intrusive") || - N.contains_insensitive("shared")) { + N.contains_insensitive("shared") || N.ends_with_insensitive("rc")) { return true; } } @@ -3388,13 +3443,24 @@ PathDiagnosticPieceRef MallocBugVisitor::VisitNode(const ExplodedNode *N, // original reference count is positive, we should not report use-after-frees // on objects deleted in such destructors. This can probably be improved // through better shared pointer modeling. - if (ReleaseDestructorLC) { + if (ReleaseDestructorLC && (ReleaseDestructorLC == CurrentLC || + ReleaseDestructorLC->isParentOf(CurrentLC))) { if (const auto *AE = dyn_cast<AtomicExpr>(S)) { + // Check for manual use of atomic builtins. AtomicExpr::AtomicOp Op = AE->getOp(); if (Op == AtomicExpr::AO__c11_atomic_fetch_add || Op == AtomicExpr::AO__c11_atomic_fetch_sub) { - if (ReleaseDestructorLC == CurrentLC || - ReleaseDestructorLC->isParentOf(CurrentLC)) { + BR.markInvalid(getTag(), S); + } + } else if (const auto *CE = dyn_cast<CallExpr>(S)) { + // Check for `std::atomic` and such. This covers both regular method calls + // and operator calls. + if (const auto *MD = + dyn_cast_or_null<CXXMethodDecl>(CE->getDirectCallee())) { + const CXXRecordDecl *RD = MD->getParent(); + // A bit wobbly with ".contains()" because it may be like + // "__atomic_base" or something. + if (StringRef(RD->getNameAsString()).contains("atomic")) { BR.markInvalid(getTag(), S); } } @@ -3628,3 +3694,4 @@ REGISTER_CHECKER(MallocChecker) REGISTER_CHECKER(NewDeleteChecker) REGISTER_CHECKER(NewDeleteLeaksChecker) REGISTER_CHECKER(MismatchedDeallocatorChecker) +REGISTER_CHECKER(TaintedAllocChecker) |