5

I am using clang static analyzer 4.0.0. For the following example

int fun(){

    int aa = 1,bb = 0;
    int cc = aa/bb; // 1) devide by zero. // Reported by clang

    int *pt = nullptr;
    int a = *pt;    // 2) null pointer dereference. // NOT Reported by clang

    int b;
    int c = a + b;  // 3) Unused initialization. // Reported by clang

    return cc;
}

Clang static analyzer reports only two issues 1 and 3 and skips issue 2.

Whereas if I changed the order of issue like this

int fun(){

    int *pt = nullptr;
    int a = *pt;    // 1) null pointer dereference. // Reported by clang

    int aa = 1,bb = 0;
    int cc = aa/bb; // 2) devide by zero. // NOT Reported by clang

    int b;
    int c = a + b;  // 3) Unused initialization. // Reported by clang

    return cc;
}

then clang static analyzer reports 1 and 3 and skips 2.

I am running clang static analyzer with this command

clang-check.exe -analyze D:\testsrc\anothercpp.cpp

This is very inconsistent behavior. No matter in what order the issues are, one of the issue get skipped. Also, I checked this scenario with clang 5.0.1 only to yield same results.

Does anybody have any idea why this is happening with static analyzer?

Thanks in advance.

-Hemant

Hemant
  • 767
  • 6
  • 20
  • checked with latest 5.0.1 gives same result – Hemant Feb 09 '18 at 11:40
  • 1
    I wonder if this is not random at all but may be related to the first issue already potentially causing all kinds of havoc (Either of your first issues is undefined behaviour and will trap on most systems). This may throw off the subsequent analysis. I could also imagine that further flow sensitive checks (where the values of variables are tracked) are turned off on that path, because they may report bogus errors in general. Error 3 can be reliably detected without ever performing that type of analysis, so it is always reported. But that is just speculation. – PaulR Feb 09 '18 at 12:01
  • Looks like the analyzer stops processing after the first occurrence of some of the checks. Not sure exactly how done, but if they do two passes - the one with the "null pointer dereference" and "devide by zero" breaks after the first error. You can even try it withe the same issue twice in a row - e.g. it stops ate the first of two "devide by zero". – Mihayl Feb 09 '18 at 12:08
  • If I add a second null pointer dereference after first one, then the second is not reported as null-pointer-dereference but unused variable – Hemant Feb 09 '18 at 12:13
  • And if you want to dive in https://github.com/llvm-mirror/clang/tree/master/lib/StaticAnalyzer – Mihayl Feb 09 '18 at 12:17

2 Answers2

4

Having a quick look at the code it seems the behaviour you observed is by design.

When the DereferenceChecker which presumably found the null-pointer dereference reports a bug, it creates an "error node" that stops further exploration for path-sensitive analyses.

void DereferenceChecker::reportBug(ProgramStateRef State, const Stmt *S,
                                   CheckerContext &C) const {
  // Generate an error node.
ExplodedNode *N = C.generateErrorNode(State);

CheckerContext::generateErrorNode is documented to stop exploration of the given path through the program.

  /// \brief Generate a transition to a node that will be used to report
  /// an error. This node will be a sink. That is, it will stop exploration of
  /// the given path.
  ///
  /// @param State The state of the generated node.
  /// @param Tag The tag to uniquely identify the creation site. If null,
  ///        the default tag for the checker will be used.
  ExplodedNode *generateErrorNode(ProgramStateRef State = nullptr,
                                  const ProgramPointTag *Tag = nullptr) {
    return generateSink(State, Pred,
                       (Tag ? Tag : Location.getTag()));
}

This makes sense, because after an error as grave as a null-pointer dereference, not many meaningful predictions about a program can be made. Since null-pointer dereference is undefined behaviour in C++, the standard allows anything to happen. Only by looking at the details of the program and the environment it is running on, more predictions could be made. Likely these predictions would be out of scope for the static analyzer.

In practice you can only fix one error at a time and would likely continue fixing errors until the static analyzer stops making valid complaints.

Detecting an unused variable does not require a path-sensitive analysis. Therefore this type of checker will still work.

PaulR
  • 3,587
  • 14
  • 24
1

All this demonstrates that you can't simply write a few primitive artificial tests for a static analyzer. I discussed this in detail in the article "Why I Dislike Synthetic Tests". It's most likely that for some internal reason (but surely not because of an error), the Clang analyzer skips the code fragment that follows a null dereferencing operation, which obviously leads to undefined behavior. Or it may skip because of a division by zero. And there's nothing wrong about it. It doesn't actually matter what's written after a null dereferencing or division by zero: the subsequent code doesn't execute anyway. So, this problem has nothing to do with the fault of the compiler/analyzer, but is rather the result of carelessly written tests like that. Writing a good test for a diagnostic is a difficult job, which requires you to be careful and aware of the many subtle details.

AndreyKarpov
  • 1,083
  • 6
  • 17