[cfe-dev] [StaticAnalyzer] Can it detect __builtin_trap() ?

Artem Dergachev via cfe-dev cfe-dev at lists.llvm.org
Tue Jun 6 03:23:19 PDT 2017


We do not currently warn on those, apart from debug.ExprInspection 
checker that warns whenever void clang_analyzer_warnIfReached() is 
called (we use this for regression tests).

It should be easy to write the checker you want. It'd probably subscribe 
on checkPreCall, see if the builtin is being called, and if there are 
arguments it may check if symbolic values for these arguments can be 
assumed to be true or false on this path, and then throw a report. You 
may look at BuiltinFunctionsChecker and NoReturnFunctionsChecker for 
examples.

--

However, we most likely wouldn't ever be able to warn on violating the 
assumptions by default, because we believe that it'd cause a lot more 
false positives than useful reports. Instead, for now we have to rely on 
these functions to avoid false positives by making the analyzer trust 
the assertions added by developers. This decision is not entirely 
pleasant - i totally see what you're thinking - but for now we have to 
keep a good practical balance between our ability to find bugs and avoid 
false positives.

The whole point of the analyzer is to find paths through the program 
that lead to various bad events, like division by zero or null 
dereference. If a certain syntactic construct, like a trap() call, 
immediately constitutes a failure, then the analyzer would essentially 
always warn on every trap() call unless he's sure it's dead code - and 
the analyzer tries to avoid seeing dead code as much as he can, instead 
believing that bugs indeed happen, because that's essentially how it 
finds most of the bugs, and this is the whole point of symbolic execution.

Consider an example:

void foo(x) {
   if (x == 42)
     trap();
}

If we analyze this function as a top-level function, as opposed to being 
called from another function (as we cannot afford whole-program analysis 
- it's ridiculously hyperexponential), then we'd assume that x is equal 
to 42, because the if-statement is there for a reason, and then we'd 
warn that a trap may be triggered if x is equal to 42. However, that's 
not what we intend to see. Instead, we should use trap() to notify us 
that the branch shouldn't be analyzed.


Consider another example:

   if (x == 1) {
     // ...
   }
   if (y == 2) {
     // ...
   }
   if (y == 2) {
     assume(x == 1);
   }

On the first if(), we can safely assume that both (x == 1) and (x != 1) 
is possible. Otherwise, the if() would be a dead code, and we presume 
that there's no dead code (which is, again, the whole point of symbolic 
execution).

On the second if(), we assume that all four cases are possible: (x == 1, 
y == 2), (x == 1, y != 2), (x != 1, y == 2), (x != 1, y != 2). This 
assumption is not entirely safe anymore: it might be that only the 
second and the third case take place, and there's still no dead code. 
However, we take this risk in order to find more bugs and reduce the 
analyzer's complexity, because making a completely safe assumption that 
is still useful would be very hard.

On the third if(), we are entering from two of the four branches that 
already exist: (x == 1, y == 2), (x != 1, y == 2). When we encounter 
assume(), we throw away (x != 1, y == 2). This compensates for the 
unsafety on the second if(). This way developers can suppress our unsafe 
branches.

In case of loops it gets even worse - we've no idea how many times a 
loop may execute.

So generally the tradeoff is:
- For various reasons, we sometimes allow ourselves to explore more 
paths than we should. This includes eager state splits, conservative 
evaluation of functions, lack of knowledge about the initial state of 
the program, shortcomings of our fast constraint solver that sometimes 
fails to see that certain constraints are incompatibly, etc.
- We then allow the developers to suppress unwanted paths with various 
no-return functions.

--

Regardless of the problems discussed above, i believe that your use case 
has a point. In some cases, we should be able to clearly see that the 
code does indeed violate the assumption. An obvious case would be 
assume(false). Or we should be able to warn on foo(42), where foo(x) is 
the function from the first example. Such warning would be a lot more 
complex than what we've originally expected though. For example, we 
won't be able to easily discriminate between "bool a = false; 
assume(a);" and "assume(x == 1);" in the second example on branch (x != 
1, y == 2), because in both cases we have plain "false" as the symbolic 
value we're assuming. Similarly, if someone passes 42 to foo, it'd be 
very similar to analyzing this function as top-level and assuming x == 
42 at the if-statement, however our checker would have to behave 
differently.

So, one way or another, we don't currently have a good mechanism to 
discriminate between these cases and write a really useful checker the 
way you want it.


05/06/2017 7:57 PM, Andrzej Krzemienski via cfe-dev wrote:
> Hi Everyone,
> Does Clan Static Analyzer recognize `__builtin_trap()` or 
> `__builtin_assume()` or `__builtin_unreachable()` or some similar thin 
> that indicates undesired failure? So that I (library author) could 
> assist the analyzer in reporting bad usages of a library:
>
> ```
> T& optional<T>::value()
> {
>   if (!this->is_initialized)
>     __builtin_trap(); // if analyzer reaches this path, it should 
> report a warnin
>   return reinterpret_cast<T&>(_raw_storage);
> }
> ```
>
> Regards,
> &rzej;
>
>
>
> _______________________________________________
> cfe-dev mailing list
> cfe-dev at lists.llvm.org
> http://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-dev




More information about the cfe-dev mailing list