NoQ created this revision. NoQ added reviewers: dcoughlin, xazax.hun, rnkovacs, mikhail.ramalho, Szelethus, baloghadamsoftware, Charusso. Herald added subscribers: cfe-commits, jdoerfert, dkrupp, donat.nagy, a.sidorin, szepet. Herald added a project: clang.
Hmm, i literally patched the same line of code a few days ago in D59901 <https://reviews.llvm.org/D59901>. That's fairly accidental. `NoStoreFuncVisitor` is mostly attached to uninitialized value reports and is responsible for adding path notes within (a.k.a. disabling pruning of) inlined calls that could have initialized the memory region but didn't end up doing it. It emits notes that say `Returning without writing to ...` at the respective return sites of such calls. George decided to suppress the note for calls into system headers. I guess the reason was that it was otherwise too loud. After all, it's an un-pruning effort - it can cause massive bring-ins of inlined calls into reports. However, when the note is suppressed, the very original issue that caused us to write this visitor bites us again: the report becomes incomprehensible. Here's a specific example i'm looking at: #include <iostream> void use(char); void foo() { char A; std::cin >> A; use(A); // Use of uninitialized variable?! } This is a "true" positive: there are a bunch of failure modes in `std::cin` that may lead to not initializing the variable and the developer has to check for them before using the variable. However, - You'll never be able to understand that from the report; - Even if it's true, the user would most likely still not bother fixing it unless it's a security-critical application. What our options here? 1. We can model `operator>>()`. We can either model it as something that always initializes the value to an unknown (and possibly tainted) value, or force a state split with a specific note about the contract of the operator, such as `Value A may remain uninitialized when B is called (ρ=0.56), given C, assuming D and under E conditions` (we'll have to also make sure that these preconditions are compatible with the current state). That's the ideal solution because it gives us the perfect modeling that we want and gives us ultimate flexibility. 2. We can disable inlining of `operator>>()` and maybe other system header functions that take out-parameters. 3. We can suppress bug reports that would have caused the no-store visitor to emit its notes in system headers. In this patch i'm implementing solution 3. Solution 1 is not incompatible with solutions 2 and 3; it can be incrementally added on top of either solution 2 or solution 3 as a more man-hour-expensive incremental improvement (suppress inlining/reporting but also unsuppress by modeling a few known functions explicitly). Solution 2 is similar to our container inlining heuristic: just don't bother inlining 'cause we'll never understand what's going on anyway. I ended up hating that heuristic and dreaming of carefully removing it and replacing it with visitor-based suppressions such as the solution 3. By suppressing inlining, we have all of the downsides of the conservative evaluation: we end up exploring obviously infeasible execution paths because we're losing information about the program. Solution 3 is more targeted: it only suppresses reports of a specific checker for which the problem has actually manifested rather than all checkers, it doesn't cause arbitrary unpredictable coverage skew, and it doesn't destroy any valid information that we managed to obtain during inlining. The patch is trivial and mostly consists of inevitable renaming functions and variables. There's one interesting gotcha though: if the function has no branches whatsoever, disable the suppression. Like, if the function unconditionally fails to initialize anything, the developer probably knows about that. I think we should do more of such un-suppressions. This was inspired by a test case in `test/Analysis/new.cpp` that otherwise regressed: 200 int testNoInitializationPlacement() { 201 int n; 202 new (&n) int; // Doesn't initialize 'n'! 203 204 if (n) { // expected-warning{{Branch condition evaluates to a garbage value}} 205 return 0; 206 } 207 return 1; 208 } Repository: rC Clang https://reviews.llvm.org/D60107 Files: clang/lib/StaticAnalyzer/Core/BugReporterVisitors.cpp clang/test/Analysis/Inputs/no-store-suppression.h clang/test/Analysis/no-store-suppression.cpp
Index: clang/test/Analysis/no-store-suppression.cpp =================================================================== --- /dev/null +++ clang/test/Analysis/no-store-suppression.cpp @@ -0,0 +1,17 @@ +// RUN: %clang_analyze_cc1 -analyzer-checker=core -verify %s + +// expected-no-diagnostics + +#include "Inputs/no-store-suppression.h" + +using namespace std; + +namespace value_uninitialized_after_stream_shift { +void use(char c); + +void foo() { + char c; + std::cin >> c; + use(c); // no-warning +} +} // namespace value_uninitialized_after_stream_shift Index: clang/test/Analysis/Inputs/no-store-suppression.h =================================================================== --- /dev/null +++ clang/test/Analysis/Inputs/no-store-suppression.h @@ -0,0 +1,17 @@ +#pragma clang system_header + +namespace std { +class istream { +public: + bool is_eof(); + char get_char(); +}; + +istream &operator>>(istream &is, char &c) { + if (is.is_eof()) + return; + c = is.get_char(); +} + +extern istream cin; +}; Index: clang/lib/StaticAnalyzer/Core/BugReporterVisitors.cpp =================================================================== --- clang/lib/StaticAnalyzer/Core/BugReporterVisitors.cpp +++ clang/lib/StaticAnalyzer/Core/BugReporterVisitors.cpp @@ -306,9 +306,14 @@ ID.AddPointer(RegionOfInterest); } + void* getTag() const { + static int Tag = 0; + return static_cast<void *>(&Tag); + } + std::shared_ptr<PathDiagnosticPiece> VisitNode(const ExplodedNode *N, BugReporterContext &BR, - BugReport &) override { + BugReport &R) override { const LocationContext *Ctx = N->getLocationContext(); const StackFrameContext *SCtx = Ctx->getStackFrame(); @@ -322,9 +327,6 @@ CallEventRef<> Call = BR.getStateManager().getCallEventManager().getCaller(SCtx, State); - if (Call->isInSystemHeader()) - return nullptr; - // Region of interest corresponds to an IVar, exiting a method // which could have written into that IVar, but did not. if (const auto *MC = dyn_cast<ObjCMethodCall>(Call)) { @@ -333,8 +335,8 @@ if (RegionOfInterest->isSubRegionOf(SelfRegion) && potentiallyWritesIntoIvar(Call->getRuntimeDefinition().getDecl(), IvarR->getDecl())) - return notModifiedDiagnostics(N, {}, SelfRegion, "self", - /*FirstIsReferenceType=*/false, 1); + return emitNoteOrSuppress(R, *Call, N, {}, SelfRegion, "self", + /*FirstIsReferenceType=*/false, 1); } } @@ -342,8 +344,8 @@ const MemRegion *ThisR = CCall->getCXXThisVal().getAsRegion(); if (RegionOfInterest->isSubRegionOf(ThisR) && !CCall->getDecl()->isImplicit()) - return notModifiedDiagnostics(N, {}, ThisR, "this", - /*FirstIsReferenceType=*/false, 1); + return emitNoteOrSuppress(R, *Call, N, {}, ThisR, "this", + /*FirstIsReferenceType=*/false, 1); // Do not generate diagnostics for not modified parameters in // constructors. @@ -359,21 +361,20 @@ int IndirectionLevel = 1; QualType T = PVD->getType(); - while (const MemRegion *R = S.getAsRegion()) { - if (RegionOfInterest->isSubRegionOf(R) && !isPointerToConst(T)) - return notModifiedDiagnostics(N, {}, R, ParamName, - ParamIsReferenceType, IndirectionLevel); + while (const MemRegion *MR = S.getAsRegion()) { + if (RegionOfInterest->isSubRegionOf(MR) && !isPointerToConst(T)) + return emitNoteOrSuppress(R, *Call, N, {}, MR, ParamName, + ParamIsReferenceType, IndirectionLevel); QualType PT = T->getPointeeType(); if (PT.isNull() || PT->isVoidType()) break; if (const RecordDecl *RD = PT->getAsRecordDecl()) - if (auto P = findRegionOfInterestInRecord(RD, State, R)) - return notModifiedDiagnostics(N, *P, RegionOfInterest, ParamName, - ParamIsReferenceType, - IndirectionLevel); + if (auto P = findRegionOfInterestInRecord(RD, State, MR)) + return emitNoteOrSuppress(R, *Call, N, *P, RegionOfInterest, ParamName, + ParamIsReferenceType, IndirectionLevel); - S = State->getSVal(R, PT); + S = State->getSVal(MR, PT); T = PT; IndirectionLevel++; } @@ -545,9 +546,29 @@ /// \return Diagnostics piece for region not modified in the current function. std::shared_ptr<PathDiagnosticPiece> - notModifiedDiagnostics(const ExplodedNode *N, const RegionVector &FieldChain, - const MemRegion *MatchedRegion, StringRef FirstElement, - bool FirstIsReferenceType, unsigned IndirectionLevel) { + emitNoteOrSuppress(BugReport &R, const CallEvent &Call, const ExplodedNode *N, + const RegionVector &FieldChain, + const MemRegion *MatchedRegion, StringRef FirstElement, + bool FirstIsReferenceType, unsigned IndirectionLevel) { + // Optimistically suppress uninitialized value bugs that result + // from system headers having a chance to initialize the value + // but failing to do so. It's too unlikely a system header's fault. + // It's much more likely a situation in which the function has a failure + // mode that the user decided not to check. If we want to hunt such + // omitted checks, we should provide an explicit function-specific note + // describing the precondition under which the function isn't supposed to + // initialize its out-parameter, and additionally check that such + // precondition can actually be fulfilled on the current path. + if (Call.isInSystemHeader()) { + // We make an exception for system header functions that have no branches, + // i.e. have exactly 3 CFG blocks: begin, all its code, end. Such + // functions unconditionally fail to initialize the variable. If they call + // other functions that have more paths within them, this suppression + // would still apply when we visit these inner functions. + if (N->getStackFrame()->getCFG()->size() > 3) + R.markInvalid(getTag(), nullptr); + return nullptr; + } PathDiagnosticLocation L = PathDiagnosticLocation::create(N->getLocation(), SM);
_______________________________________________ cfe-commits mailing list cfe-commits@lists.llvm.org https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits