Index: clang/include/clang/StaticAnalyzer/Core/RetainSummaryManager.h =================================================================== --- clang/include/clang/StaticAnalyzer/Core/RetainSummaryManager.h +++ clang/include/clang/StaticAnalyzer/Core/RetainSummaryManager.h @@ -77,13 +77,21 @@ IncRef, /// The argument is a pointer to a retain-counted object; on exit, the new - /// value of the pointer is a +0 value or NULL. + /// value of the pointer is a +0 value. UnretainedOutParameter, /// The argument is a pointer to a retain-counted object; on exit, the new - /// value of the pointer is a +1 value or NULL. + /// value of the pointer is a +1 value. RetainedOutParameter, + /// The argument is a pointer to a retain-counted object; on exit, the new + /// value of the pointer is a +1 value iff the return code is zero. + RetainedOutParameterOnZero, + + /// The argument is a pointer to a retain-counted object; on exit, the new + /// value of the pointer is a +1 value iff the return code is non-zero. + RetainedOutParameterOnNonZero, + /// The argument is treated as potentially escaping, meaning that /// even when its reference count hits 0 it should be treated as still /// possibly being alive as someone else *may* be holding onto the object. Index: clang/lib/StaticAnalyzer/Checkers/RetainCountChecker/RetainCountChecker.cpp =================================================================== --- clang/lib/StaticAnalyzer/Checkers/RetainCountChecker/RetainCountChecker.cpp +++ clang/lib/StaticAnalyzer/Checkers/RetainCountChecker/RetainCountChecker.cpp @@ -529,38 +529,80 @@ C.addTransition(state); } -static ProgramStateRef updateOutParameter(ProgramStateRef State, - SVal ArgVal, - ArgEffectKind Effect) { - auto *ArgRegion = dyn_cast_or_null(ArgVal.getAsRegion()); - if (!ArgRegion) - return State; - - QualType PointeeTy = ArgRegion->getValueType(); - if (!coreFoundation::isCFObjectRef(PointeeTy)) - return State; - - SVal PointeeVal = State->getSVal(ArgRegion); - SymbolRef Pointee = PointeeVal.getAsLocSymbol(); - if (!Pointee) - return State; - - switch (Effect) { - case UnretainedOutParameter: - State = setRefBinding(State, Pointee, - RefVal::makeNotOwned(ObjKind::CF, PointeeTy)); - break; - case RetainedOutParameter: - // Do nothing. Retained out parameters will either point to a +1 reference - // or NULL, but the way you check for failure differs depending on the API. - // Consequently, we don't have a good way to track them yet. - break; +static SmallVector +updateOutParameters(ProgramStateRef State, const RetainSummary &Summ, + const CallEvent &CE) { + SVal L = CE.getReturnValue(); + + // Splitting is required to support out parameters, + // as out parameters might be created only on the "success" branch. + // We want to avoid eagerly splitting unless out parameters are actually + // needed. + bool SplitNecessary = false; + for (auto &P : Summ.getArgEffects()) + if (P.second.getKind() == RetainedOutParameterOnNonZero || + P.second.getKind() == RetainedOutParameterOnZero) + SplitNecessary = true; + + ProgramStateRef AssumeNonZeroReturn = State; + ProgramStateRef AssumeZeroReturn = State; + + if (SplitNecessary) { + if (auto DL = L.getAs()) { + AssumeNonZeroReturn = AssumeNonZeroReturn->assume(*DL, true); + AssumeZeroReturn = AssumeZeroReturn->assume(*DL, false); + } + } + + for (unsigned idx = 0, e = CE.getNumArgs(); idx != e; ++idx) { + SVal ArgVal = CE.getArgSVal(idx); + ArgEffect AE = Summ.getArg(idx); + + auto *ArgRegion = dyn_cast_or_null(ArgVal.getAsRegion()); + if (!ArgRegion) + continue; - default: - llvm_unreachable("only for out parameters"); + QualType PointeeTy = ArgRegion->getValueType(); + SVal PointeeVal = State->getSVal(ArgRegion); + SymbolRef Pointee = PointeeVal.getAsLocSymbol(); + if (!Pointee) + continue; + + auto makeNotOwnedParameter = [&](ProgramStateRef St) { + return setRefBinding(St, Pointee, + RefVal::makeNotOwned(AE.getObjKind(), PointeeTy)); + }; + auto makeOwnedParameter = [&](ProgramStateRef St) { + return setRefBinding(St, Pointee, + RefVal::makeOwned(ObjKind::OS, PointeeTy)); + }; + + switch (AE.getKind()) { + case UnretainedOutParameter: + AssumeNonZeroReturn = makeNotOwnedParameter(AssumeNonZeroReturn); + AssumeZeroReturn = makeNotOwnedParameter(AssumeZeroReturn); + break; + case RetainedOutParameter: + AssumeNonZeroReturn = makeOwnedParameter(AssumeNonZeroReturn); + AssumeZeroReturn = makeOwnedParameter(AssumeZeroReturn); + break; + case RetainedOutParameterOnNonZero: + AssumeNonZeroReturn = makeOwnedParameter(AssumeNonZeroReturn); + break; + case RetainedOutParameterOnZero: + AssumeZeroReturn = makeOwnedParameter(AssumeZeroReturn); + break; + default: + break; + } } - return State; + if (SplitNecessary) { + return {AssumeNonZeroReturn, AssumeZeroReturn}; + } else { + assert(AssumeZeroReturn == AssumeNonZeroReturn); + return {AssumeZeroReturn}; + } } void RetainCountChecker::checkSummary(const RetainSummary &Summ, @@ -582,10 +624,7 @@ SVal V = CallOrMsg.getArgSVal(idx); ArgEffect Effect = Summ.getArg(idx); - if (Effect.getKind() == RetainedOutParameter || - Effect.getKind() == UnretainedOutParameter) { - state = updateOutParameter(state, V, Effect.getKind()); - } else if (SymbolRef Sym = V.getAsLocSymbol()) { + if (SymbolRef Sym = V.getAsLocSymbol()) { if (const RefVal *T = getRefBinding(state, Sym)) { if (shouldEscapeOSArgumentOnCall(CallOrMsg, idx, T)) @@ -661,10 +700,15 @@ state = setRefBinding(state, Sym, *updatedRefVal); } - if (DeallocSent) { - C.addTransition(state, C.getPredecessor(), &DeallocSentTag); - } else { - C.addTransition(state); + SmallVector Out = + updateOutParameters(state, Summ, CallOrMsg); + + for (ProgramStateRef St : Out) { + if (DeallocSent) { + C.addTransition(St, C.getPredecessor(), &DeallocSentTag); + } else { + C.addTransition(St); + } } } @@ -700,6 +744,8 @@ switch (AE.getKind()) { case UnretainedOutParameter: case RetainedOutParameter: + case RetainedOutParameterOnZero: + case RetainedOutParameterOnNonZero: llvm_unreachable("Applies to pointer-to-pointer parameters, which should " "not have ref state."); Index: clang/lib/StaticAnalyzer/Checkers/RetainCountChecker/RetainCountDiagnostics.cpp =================================================================== --- clang/lib/StaticAnalyzer/Checkers/RetainCountChecker/RetainCountDiagnostics.cpp +++ clang/lib/StaticAnalyzer/Checkers/RetainCountChecker/RetainCountDiagnostics.cpp @@ -113,11 +113,31 @@ return true; } +/// Finds argument index of the out paramter in the call {@code S} +/// corresponding to the symbol {@code Sym}. +/// If none found, returns None. +static Optional findArgIdxOfSymbol(ProgramStateRef CurrSt, + const LocationContext *LCtx, + SymbolRef &Sym, + Optional> CE) { + if (!CE) + return None; + + for (unsigned Idx = 0; Idx < (*CE)->getNumArgs(); Idx++) + if (const MemRegion *MR = (*CE)->getArgSVal(Idx).getAsRegion()) + if (const auto *TR = dyn_cast(MR)) + if (CurrSt->getSVal(MR, TR->getValueType()).getAsSymExpr() == Sym) + return Idx; + + return None; +} + static void generateDiagnosticsForCallLike(ProgramStateRef CurrSt, const LocationContext *LCtx, const RefVal &CurrV, SymbolRef &Sym, const Stmt *S, llvm::raw_string_ostream &os) { + CallEventManager &Mgr = CurrSt->getStateManager().getCallEventManager(); if (const CallExpr *CE = dyn_cast(S)) { // Get the name of the callee (if it is available) // from the tracked SVal. @@ -139,7 +159,6 @@ os << "Operator 'new'"; } else { assert(isa(S)); - CallEventManager &Mgr = CurrSt->getStateManager().getCallEventManager(); CallEventRef Call = Mgr.getObjCMethodCall(cast(S), CurrSt, LCtx); @@ -156,6 +175,16 @@ } } + Optional> CE = Mgr.getCall(S, CurrSt, LCtx); + auto Idx = findArgIdxOfSymbol(CurrSt, LCtx, Sym, CE); + + // If index is not found, we assume that the symbol was returned. + if (!Idx) { + os << " returns"; + } else { + os << " writes"; + } + if (CurrV.getObjKind() == ObjKind::CF) { os << " a Core Foundation object of type '" << Sym->getType().getAsString() << "' with a "; @@ -169,10 +198,10 @@ assert(CurrV.getObjKind() == ObjKind::ObjC); QualType T = Sym->getType(); if (!isa(T)) { - os << " returns an Objective-C object with a "; + os << " an Objective-C object with a "; } else { const ObjCObjectPointerType *PT = cast(T); - os << " returns an instance of " << PT->getPointeeType().getAsString() + os << " an instance of " << PT->getPointeeType().getAsString() << " with a "; } } @@ -183,6 +212,14 @@ assert(CurrV.isNotOwned()); os << "+0 retain count"; } + + if (Idx) { + os << " into an out parameter '"; + const ParmVarDecl *PVD = (*CE)->parameters()[*Idx]; + PVD->getNameForDiagnostic(os, PVD->getASTContext().getPrintingPolicy(), + /*Qualified=*/false); + os << "'"; + } } namespace clang { Index: clang/lib/StaticAnalyzer/Core/RetainSummaryManager.cpp =================================================================== --- clang/lib/StaticAnalyzer/Core/RetainSummaryManager.cpp +++ clang/lib/StaticAnalyzer/Core/RetainSummaryManager.cpp @@ -88,7 +88,9 @@ return None; K = ObjKind::ObjC; } else if (isOneOf()) { + OSReturnsNotRetainedAttr, OSReturnsRetainedAttr, + OSReturnsRetainedOnZeroAttr, + OSReturnsRetainedOnNonZeroAttr>()) { if (!TrackOSObjects) return None; K = ObjKind::OS; @@ -522,6 +524,8 @@ case IncRef: case UnretainedOutParameter: case RetainedOutParameter: + case RetainedOutParameterOnZero: + case RetainedOutParameterOnNonZero: case MayEscape: case StopTracking: case StopTrackingHard: @@ -811,6 +815,29 @@ return None; } +/// \return Whether the chain of typedefs starting from {@code QT} +/// has a typedef with a given name {@code Name}. +static bool hasTypedefNamed(QualType QT, + StringRef Name) { + while (auto *T = dyn_cast(QT)) { + const auto &Context = T->getDecl()->getASTContext(); + if (T->getDecl()->getIdentifier() == &Context.Idents.get(Name)) + return true; + QT = T->getDecl()->getUnderlyingType(); + } + return false; +} + +static QualType getCallableReturnType(const NamedDecl *ND) { + if (const auto *FD = dyn_cast(ND)) { + return FD->getReturnType(); + } else if (const auto *MD = dyn_cast(ND)) { + return MD->getReturnType(); + } else { + llvm_unreachable("Unexpected decl"); + } +} + bool RetainSummaryManager::applyParamAnnotationEffect( const ParmVarDecl *pd, unsigned parm_idx, const NamedDecl *FD, RetainSummaryTemplate &Template) { @@ -820,21 +847,54 @@ GeneralizedConsumedAttr>(pd, QT)) { Template->addArg(AF, parm_idx, ArgEffect(DecRef, *K)); return true; - } else if (auto K = - hasAnyEnabledAttrOf(pd, QT)) { - Template->addArg(AF, parm_idx, ArgEffect(RetainedOutParameter, *K)); + } else if (auto K = hasAnyEnabledAttrOf< + CFReturnsRetainedAttr, OSReturnsRetainedAttr, + OSReturnsRetainedOnNonZeroAttr, OSReturnsRetainedOnZeroAttr, + GeneralizedReturnsRetainedAttr>(pd, QT)) { + + // For OSObjects, we try to guess whether the object is created based + // on the return value. + if (K == ObjKind::OS) { + QualType QT = getCallableReturnType(FD); + + bool HasRetainedOnZero = pd->hasAttr(); + bool HasRetainedOnNonZero = pd->hasAttr(); + + // The usual convention is to create an object on non-zero return, but + // it's reverted if the typedef chain has a typedef kern_return_t, + // becauese kReturnSuccess constant is defined as zero. + // The convention can be overwritten by custom attributes. + bool SuccessOnZero = + HasRetainedOnZero || + (hasTypedefNamed(QT, "kern_return_t") && !HasRetainedOnNonZero); + bool ShouldSplit = !QT.isNull() && !QT->isVoidType(); + ArgEffectKind AK = RetainedOutParameter; + if (ShouldSplit && SuccessOnZero) { + AK = RetainedOutParameterOnZero; + } else if (ShouldSplit && (!SuccessOnZero || HasRetainedOnNonZero)) { + AK = RetainedOutParameterOnNonZero; + } + Template->addArg(AF, parm_idx, ArgEffect(AK, ObjKind::OS)); + } + + // For others: + // Do nothing. Retained out parameters will either point to a +1 reference + // or NULL, but the way you check for failure differs depending on the + // API. Consequently, we don't have a good way to track them yet. return true; - } else if (auto K = hasAnyEnabledAttrOf(pd, QT)) { + } else if (auto K = hasAnyEnabledAttrOf( + pd, QT)) { Template->addArg(AF, parm_idx, ArgEffect(UnretainedOutParameter, *K)); return true; - } else { - if (const auto *MD = dyn_cast(FD)) { - for (const auto *OD : MD->overridden_methods()) { - const ParmVarDecl *OP = OD->parameters()[parm_idx]; - if (applyParamAnnotationEffect(OP, parm_idx, OD, Template)) - return true; - } + } + + if (const auto *MD = dyn_cast(FD)) { + for (const auto *OD : MD->overridden_methods()) { + const ParmVarDecl *OP = OD->parameters()[parm_idx]; + if (applyParamAnnotationEffect(OP, parm_idx, OD, Template)) + return true; } } Index: clang/test/Analysis/osobject-retain-release.cpp =================================================================== --- clang/test/Analysis/osobject-retain-release.cpp +++ clang/test/Analysis/osobject-retain-release.cpp @@ -5,6 +5,8 @@ #define OS_CONSUME __attribute__((os_consumed)) #define OS_RETURNS_RETAINED __attribute__((os_returns_retained)) +#define OS_RETURNS_RETAINED_ON_ZERO __attribute__((os_returns_retained_on_zero)) +#define OS_RETURNS_RETAINED_ON_NONZERO __attribute__((os_returns_retained_on_non_zero)) #define OS_RETURNS_NOT_RETAINED __attribute__((os_returns_not_retained)) #define OS_CONSUMES_THIS __attribute__((os_consumes_this)) @@ -94,6 +96,153 @@ void escape_with_source(void *p) {} bool coin(); +typedef int kern_return_t; +typedef kern_return_t IOReturn; +typedef kern_return_t OSReturn; +#define kOSReturnSuccess 0 +#define kIOReturnSuccess 0 + +bool write_into_out_param_on_success(OS_RETURNS_RETAINED OSObject **obj); + +void use_out_param() { + OSObject *obj; + if (write_into_out_param_on_success(&obj)) { + obj->release(); + } +} + +void use_out_param_leak() { + OSObject *obj; + write_into_out_param_on_success(&obj); // expected-note{{Call to function 'write_into_out_param_on_success' writes an OSObject of type 'OSObject' with a +1 retain count into an out parameter 'obj'}} +} // expected-warning{{Potential leak of an object stored into 'obj'}} + // expected-note@-1{{Object leaked: object allocated and stored into 'obj' is not referenced later in this execution path and has a retain count of +1}} + + +bool write_into_out_param_on_failure(OS_RETURNS_RETAINED_ON_ZERO OSObject **obj); + +void use_out_param_on_failure() { + OSObject *obj; + if (!write_into_out_param_on_failure(&obj)) { + obj->release(); + } +} + +IOReturn write_into_out_param_on_nonzero(OS_RETURNS_RETAINED_ON_NONZERO OSObject **obj); + +void use_out_param_on_nonzero() { + OSObject *obj; + if (write_into_out_param_on_nonzero(&obj) != kIOReturnSuccess) { + obj->release(); + } +} + +bool write_into_two_out_params(OS_RETURNS_RETAINED OSObject **a, + OS_RETURNS_RETAINED OSObject **b); + +void use_write_into_two_out_params() { + OSObject *obj1; + OSObject *obj2; + if (write_into_two_out_params(&obj1, &obj2)) { + obj1->release(); + obj2->release(); + } +} + +void use_write_two_out_params_leak() { + OSObject *obj1; + OSObject *obj2; + write_into_two_out_params(&obj1, &obj2); // expected-note{{Call to function 'write_into_two_out_params' writes an OSObject of type 'OSObject' with a +1 retain count into an out parameter 'a'}} + // expected-note@-1{{Call to function 'write_into_two_out_params' writes an OSObject of type 'OSObject' with a +1 retain count into an out parameter 'b'}} +} // expected-warning{{Potential leak of an object stored into 'obj1'}} + // expected-warning@-1{{Potential leak of an object stored into 'obj2'}} + // expected-note@-2{{Object leaked: object allocated and stored into 'obj1' is not referenced later in this execution path and has a retain count of +1}} + // expected-note@-3{{Object leaked: object allocated and stored into 'obj2' is not referenced later in this execution path and has a retain count of +1}} + +void always_write_into_two_out_params(OS_RETURNS_RETAINED OSObject **a, + OS_RETURNS_RETAINED OSObject **b); + +void use_always_write_into_two_out_params() { + OSObject *obj1; + OSObject *obj2; + always_write_into_two_out_params(&obj1, &obj2); + obj1->release(); + obj2->release(); +} + +void use_always_write_into_two_out_params_leak() { + OSObject *obj1; + OSObject *obj2; + always_write_into_two_out_params(&obj1, &obj2); // expected-note{{Call to function 'always_write_into_two_out_params' writes an OSObject of type 'OSObject' with a +1 retain count into an out parameter 'a'}} + // expected-note@-1{{Call to function 'always_write_into_two_out_params' writes an OSObject of type 'OSObject' with a +1 retain count into an out parameter 'b'}} +} // expected-warning{{Potential leak of an object stored into 'obj1'}} + // expected-warning@-1{{Potential leak of an object stored into 'obj2'}} + // expected-note@-2{{Object leaked: object allocated and stored into 'obj1' is not referenced later in this execution path and has a retain count of +1}} + // expected-note@-3{{Object leaked: object allocated and stored into 'obj2' is not referenced later in this execution path and has a retain count of +1}} + +char *write_into_out_param_on_nonnull(OS_RETURNS_RETAINED OSObject **obj); + +void use_out_param_osreturn_on_nonnull() { + OSObject *obj; + if (write_into_out_param_on_nonnull(&obj)) { + obj->release(); + } +} + +void use_out_param_leak_osreturn_on_nonnull() { + OSObject *obj; + write_into_out_param_on_nonnull(&obj); // expected-note{{Call to function 'write_into_out_param_on_nonnull' writes an OSObject of type 'OSObject' with a +1 retain count into an out parameter 'obj'}} +} // expected-warning{{Potential leak of an object stored into 'obj'}} + // expected-note@-1{{Object leaked: object allocated and stored into 'obj' is not referenced later in this execution path and has a retain count of +1}} + +bool write_optional_out_param(OS_RETURNS_RETAINED OSObject **obj=nullptr); + +void use_optional_out_param() { + if (write_optional_out_param()) {}; +} + +OSReturn write_into_out_param_on_os_success(OS_RETURNS_RETAINED OSObject **obj); + +void write_into_non_retained_out_param(OS_RETURNS_NOT_RETAINED OSObject **obj); + +void use_write_into_non_retained_out_param() { + OSObject *obj; + write_into_non_retained_out_param(&obj); +} + +void use_write_into_non_retained_out_param_uaf() { + OSObject *obj; + write_into_non_retained_out_param(&obj); // expected-note{{Call to function 'write_into_non_retained_out_param' writes an OSObject of type 'OSObject' with a +0 retain count into an out parameter 'obj'}} + obj->release(); // expected-warning{{Incorrect decrement of the reference count of an object that is not owned at this point by the caller}} + // expected-note@-1{{Incorrect decrement of the reference count of an object that is not owned at this point by the caller}} +} + +void always_write_into_out_param(OS_RETURNS_RETAINED OSObject **obj); + +void use_void_out_param_osreturn() { + OSObject *obj; + always_write_into_out_param(&obj); + obj->release(); +} + +void use_void_out_param_osreturn_leak() { + OSObject *obj; + always_write_into_out_param(&obj); // expected-note{{Call to function 'always_write_into_out_param' writes an OSObject of type 'OSObject' with a +1 retain count into an out parameter 'obj'}} +} // expected-warning{{Potential leak of an object stored into 'obj'}} + // expected-note@-1{{Object leaked: object allocated and stored into 'obj' is not referenced later in this execution path and has a retain count of +1}} + +void use_out_param_osreturn() { + OSObject *obj; + if (write_into_out_param_on_os_success(&obj) == kOSReturnSuccess) { + obj->release(); + } +} + +void use_out_param_leak_osreturn() { + OSObject *obj; + write_into_out_param_on_os_success(&obj); // expected-note{{Call to function 'write_into_out_param_on_os_success' writes an OSObject of type 'OSObject' with a +1 retain count into an out parameter 'obj'}} +} // expected-warning{{Potential leak of an object stored into 'obj'}} + // expected-note@-1{{Object leaked: object allocated and stored into 'obj' is not referenced later in this execution path and has a retain count of +1}} + bool os_consume_violation_two_args(OS_CONSUME OSObject *obj, bool extra) { if (coin()) { // expected-note{{Assuming the condition is false}} // expected-note@-1{{Taking false branch}} @@ -431,4 +580,3 @@ void test_escape_to_unknown_block(Blk blk) { blk(getObject()); // no-crash } -