NullAway icon indicating copy to clipboard operation
NullAway copied to clipboard

Handle exceptions in JarInfer

Open navahgar opened this issue 6 years ago • 6 comments

In the following code, JarInfer analysis concludes that the parameter o should be nonnull, which is incorrect.

private String paramCanBeNull(Object o) {
   try {
     return o.toString();
   } catch (NullPointerException e) {
     return "";
   }
}

In general, JarInfer does not seem to handle exceptions. This is because JarInfer currently uses ExceptionPrunedCfg for its analysis.

navahgar avatar Jun 06 '19 21:06 navahgar

I looked at the WALA ShrikeCFG implementation that we are using.

ShrikeCFG::computeEdges for blocks which have the last instruction as a potentially excepting instruction adds exceptional edges to every corresponding catch block and also to the exit block (in the cases when none of the catch blocks catches all the exceptions). For a basic block outside the try block, it adds an edge to the exit block because the enclosing method says it can throw an exception.

When we convert this CFG to prunedCFG we remove all the exceptional edges and unreachable nodes in the graph after removing edges. prunedCFG does not have enough information about this. But, just CFG has too many edges, which are not of our interest.

shubhamugare avatar Jun 07 '19 20:06 shubhamugare

That makes sense from a soundness perspective, of course: every method called might thrown an unchecked exception that could need to be propagated out of that method. On the other hand, those extra edges mean basically nothing dominates the exit block (since all calls just have an exceptional edge pointing to the exit, right?). That's only going to become more of a problem as we try to make our analysis more precise.

I wonder if the best we can do long-term is to special case try/catch blocks that look for NullPointerException or any of its super classes and just not take into account argument dereferences if they happen inside the try block of such construct. We would then need to handle the cases where the catch block itself performs a null dereference or re-throws the exception, though... mmm...

lazaroclapp avatar Jun 07 '19 21:06 lazaroclapp

If we are not gaining much information from the try block which can potentially throw a caught NPE. We can remove that block from the bytecode in the pre-processing step?

shubhamugare avatar Jun 07 '19 21:06 shubhamugare

If we are not gaining much information from the try block which can potentially throw a caught NPE. We can remove that block from the bytecode in the pre-processing step?

You mean, in the case where we have try {... } catch(NPE_TYPE e) { ... } we just omit that entire part of the AST?

I can think of two reasons not to do that:

  • We still want to analyze return statements in that code
  • We still might need to know under which circumstances the code in the catch block might trigger, and the @NonNull requirement might still be there, depending on what the catch block does.

In the case above, the correct answer is @Nullable Object o, but consider:

private String paramCantBeNull(Object o) {
   try {
     return o.toString();
   } catch (NullPointerException e) {
     logError(e);
     throw e;
   }
}

Or even:

private String paramCantBeNull(SomeT o) {
   try {
     return o.foo().toString();
   } catch (NullPointerException e) {
     return o.bar().toString();
   }
}

Either way o can't be null there, even if o.foo() can, so we need to mark @NonNull SomeT o.

lazaroclapp avatar Jun 07 '19 21:06 lazaroclapp

That will make it weaker. But safer than current implementation? Unless we can do something better which handles all the possible cases.

shubhamugare avatar Jun 07 '19 21:06 shubhamugare

I think keeping the current implementation with <ExceptionPrunedCFG> and adding a special case for the code in this issue (while carefully handling the cases Lazaro pointed out in https://github.com/uber/NullAway/issues/318#issuecomment-500042156) is good enough for now.

navahgar avatar Jun 07 '19 22:06 navahgar