edu.umd.cs.findbugs.detect.ConstructorThrow Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of spotbugs Show documentation
Show all versions of spotbugs Show documentation
SpotBugs: Because it's easy!
/*
* SpotBugs - Find bugs in Java programs
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package edu.umd.cs.findbugs.detect;
import java.util.*;
import java.util.stream.Collectors;
import edu.umd.cs.findbugs.OpcodeStack;
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.ba.AnalysisContext;
import edu.umd.cs.findbugs.ba.XMethod;
import edu.umd.cs.findbugs.internalAnnotations.DottedClassName;
import edu.umd.cs.findbugs.util.ClassName;
import org.apache.bcel.Const;
import edu.umd.cs.findbugs.BugInstance;
import edu.umd.cs.findbugs.BugAccumulator;
import edu.umd.cs.findbugs.BugReporter;
import edu.umd.cs.findbugs.bcel.OpcodeStackDetector;
import org.apache.bcel.classfile.ConstantPool;
import org.apache.bcel.classfile.JavaClass;
import org.apache.bcel.classfile.Method;
/**
* This detector can find constructors that throw exception.
*/
public class ConstructorThrow extends OpcodeStackDetector {
private final BugAccumulator bugAccumulator;
/**
* The containing methods (DottedClassName complete with signature) to the methods called directly from the containing one
* to the caught Exceptions by the surrounding try-catches of the call sites.
* If the call site is not inside a try-catch then an empty string.
*/
private final Map>> exHandlesToMethodCallsByMethodsMap = new HashMap<>();
/**
* The DottedClassName complete with signature of the method to the set of the Exceptions thrown directly from the method.
*/
private final Map> thrownExsByMethodMap = new HashMap<>();
private boolean isFinalClass = false;
private boolean isFinalFinalizer = false;
private boolean isFirstPass = true;
private boolean hadObjectConstructor = false;
public ConstructorThrow(BugReporter bugReporter) {
this.bugAccumulator = new BugAccumulator(bugReporter);
}
/**
* Visit a class to find the constructor, then collect all the methods that gets called in it.
* Also, we are checking for final declaration on the class, or a final finalizer, as if present
* no finalizer attack can happen.
*/
@Override
public void visit(JavaClass obj) {
resetState();
if (obj.isFinal()) {
isFinalClass = true;
return;
}
isFinalFinalizer = hasFinalFinalizer(obj);
try {
for (JavaClass cl : obj.getSuperClasses()) {
isFinalFinalizer |= hasFinalFinalizer(cl);
}
} catch (ClassNotFoundException e) {
AnalysisContext.reportMissingClass(e);
}
for (Method m : obj.getMethods()) {
hadObjectConstructor = false;
doVisitMethod(m);
}
isFirstPass = false;
}
private static boolean hasFinalFinalizer(JavaClass jc) {
// Check for final finalizer.
// Signature of the finalizer is also needed to be checked
return Arrays.stream(jc.getMethods())
.anyMatch(m -> "finalize".equals(m.getName()) && "()V".equals(m.getSignature()) && m.isFinal());
}
@Override
public void visitAfter(JavaClass obj) {
super.visit(obj);
bugAccumulator.reportAccumulatedBugs();
}
/**
* 1. Check for any throw expression in the constructor.
* 2. Check for any exception throw inside constructor, or any of the called methods.
* If the class is final, we are fine, no finalizer attack can happen.
* In the first pass the detector shouldn't report, because there could be
* a final finalizer and a throwing constructor. Reporting in this case
* would be a false positive as classes with a final finalizer are not
* vulnerable to the finalizer attack.
*/
@Override
public void sawOpcode(int seen) {
if (isFinalClass || isFinalFinalizer) {
return;
}
if (isFirstPass) {
collectExceptionsByMethods(seen);
} else if (isConstructor()) {
reportConstructorThrow(seen);
}
}
/**
* Reports ConstructorThrow bug if there is an unhandled unchecked exception thrown directly or indirectly
* from the currently visited method.
* If the exception is thrown directly, the bug is reported at the throw.
* If the exception is thrown indirectly (through a method call), the bug is reported at the call of the method
* which throws the exception.
*/
private void reportConstructorThrow(int seen) {
// if there is a throw in the Constructor which is not handled in a try-catch block, it's a bug
if (seen == Const.ATHROW) {
OpcodeStack.Item item = stack.getStackItem(0);
if (item != null) {
try {
JavaClass thrownExClass = item.getJavaClass();
// in case of try-with-resources the compiler generates a nested try-catch with Throwable
// so filter out Throwable
// however this filters out Throwable explicitly thrown by the programmer, that is not so common
if (thrownExClass == null || "java.lang.Throwable".equals(thrownExClass.getClassName())) {
return;
}
Set caughtExes = getSurroundingCaughtExes(getConstantPool());
if (isThrownExNotCaught(thrownExClass, caughtExes)) {
accumulateBug();
}
} catch (ClassNotFoundException e) {
AnalysisContext.reportMissingClass(e);
}
}
} else if (isMethodCall(seen)) {
if (Const.CONSTRUCTOR_NAME.equals(getNameConstantOperand())) {
hadObjectConstructor = true;
}
String calledMethodFQN = getCalledMethodFQN();
Set unhandledExes = getUnhandledExThrowsInMethod(calledMethodFQN, new HashSet<>());
if (hadObjectConstructor && !unhandledExes.isEmpty()) {
Set caughtExes = getSurroundingCaughtExes(getConstantPool());
boolean hasNotCaughtExFromBody = unhandledExes.stream()
.anyMatch(ex -> isThrownExNotCaught(ex, caughtExes));
if (hasNotCaughtExFromBody) {
accumulateBug();
}
}
}
}
/**
* Get the Exceptions thrown from the inside of the method, either directly or indirectly from called methods.
* Uses inner collections which are needed to filled correctly.
*
* @param method the method to visit and get the exceptions thrown out of it
* @param visitedMethods the names of the already visited methods, needed to prevent stackoverflow by recursively checking method call cycles
* @return the JavaClasses of the Exceptions thrown from the method
*/
private Set getUnhandledExThrowsInMethod(String method, Set visitedMethods) {
Set unhandledExesInMethod = new HashSet<>();
if (visitedMethods.contains(method)) {
return unhandledExesInMethod;
} else {
visitedMethods.add(method);
}
if (thrownExsByMethodMap.containsKey(method)) {
unhandledExesInMethod.addAll(thrownExsByMethodMap.get(method));
}
if (exHandlesToMethodCallsByMethodsMap.containsKey(method)) {
Map> exHandlesByMethodCalls = exHandlesToMethodCallsByMethodsMap.get(method);
for (Map.Entry> entry : exHandlesByMethodCalls.entrySet()) {
String calledMethod = entry.getKey();
Set unhandledExes = getUnhandledExThrowsInMethod(calledMethod, visitedMethods);
Set exHandles = entry.getValue();
Set remainingUnhandledExes = unhandledExes.stream()
.filter(ex -> !isHandled(ex, exHandles))
.collect(Collectors.toSet());
unhandledExesInMethod.addAll(remainingUnhandledExes);
}
}
return unhandledExesInMethod;
}
/**
* Checks whether the Exception is handled in all call sites.
* @param thrownEx the thrown Exception which needs to be handled
* @param exHandles the set of the dotted class names of the caught Exceptions in the call sites.
* @return true if the Exception handled in all call sites.
*/
private boolean isHandled(JavaClass thrownEx, Set exHandles) {
return exHandles.stream().allMatch(handle -> isHandled(thrownEx, handle));
}
/**
* Checks if the thrown Exception is handled by the caught Exception.
* @param thrownEx the thrown Exception which needs to be handled
* @param caughtEx the name of the caught Exception at the call site. If no Exception is caught,
* then it's an empty string or other nonnull string which is not a name of any Exception.
* @return true if the Exception is handled.
*/
private static boolean isHandled(JavaClass thrownEx, @NonNull @DottedClassName String caughtEx) {
try {
return thrownEx.getClassName().equals(caughtEx)
|| Arrays.stream(thrownEx.getSuperClasses()).anyMatch(e -> e.getClassName().equals(caughtEx));
} catch (ClassNotFoundException e) {
AnalysisContext.reportMissingClass(e);
}
return false;
}
/**
* Gets the DottedClassNames of the Exceptions which are caught by a try-catch block at the current PC.
* @param cp ConstantPool
* @return Set of the DottedClassNames of the caught Exceptions.
*/
private Set getSurroundingCaughtExes(ConstantPool cp) {
return getSurroundingCaughtExceptionTypes(getPC(), Integer.MAX_VALUE).stream()
.filter(i -> i != 0)
.map(caughtExType -> cp.constantToString(cp.getConstant(caughtExType)))
.collect(Collectors.toSet());
}
/**
* Checks if the thrown exception is not caught.
* @param thrownEx the Exception to catch.
* @param caughtExes the set of the DottedClassNames of the caught Exceptions at call site.
* @return true if the exception is not caught.
*/
private static boolean isThrownExNotCaught(JavaClass thrownEx, Set caughtExes) {
return caughtExes.stream().noneMatch(caughtEx -> isHandled(thrownEx, caughtEx));
}
private static String toDotted(String signature) {
if (signature.startsWith("L") && signature.endsWith(";")) {
return ClassName.toDottedClassName(signature.substring(1, signature.length() - 1));
}
return ClassName.toDottedClassName(signature);
}
/**
* Fills the inner collections while visiting the method.
* @param seen the opcode @see #sawOpcode(int)
*/
private void collectExceptionsByMethods(int seen) {
String containingMethod = getFullyQualifiedMethodName();
if (seen == Const.ATHROW) {
OpcodeStack.Item item = stack.getStackItem(0);
if (item != null) {
try {
JavaClass thrownExClass = item.getJavaClass();
// in case of try-with-resources the compiler generates a nested try-catch with Throwable
// so filter out Throwable
// however this filters out throwable explicitly thrown by the programmer, that is not so common
if (thrownExClass == null || "java.lang.Throwable".equals(thrownExClass.getClassName())) {
return;
}
Set caughtExes = getSurroundingCaughtExes(getConstantPool());
if (isThrownExNotCaught(thrownExClass, caughtExes)) {
addToThrownExsByMethodMap(containingMethod, thrownExClass);
}
} catch (ClassNotFoundException e) {
AnalysisContext.reportMissingClass(e);
}
}
} else if (isMethodCall(seen)) {
String calledMethodName = getNameConstantOperand();
String calledMethodFullName = getCalledMethodFQN();
// not interested in call of the constructor or recursion
if (!Const.CONSTRUCTOR_NAME.equals(calledMethodName) && !containingMethod.equals(calledMethodFullName)) {
Set caughtExes = getSurroundingCaughtExes(getConstantPool());
if (caughtExes.isEmpty()) {
// No Exception is handled, then add an empty string to represent this
addToExHandlesToMethodCallsByMethodsMap(containingMethod, calledMethodFullName, Collections.singletonList(""));
} else {
addToExHandlesToMethodCallsByMethodsMap(containingMethod, calledMethodFullName, caughtExes);
}
XMethod calledXMethod = getXMethodOperand();
if (calledXMethod != null) {
String[] thrownCheckedExes = calledXMethod.getThrownExceptions();
if (thrownCheckedExes != null) {
for (String thrownCheckedEx : thrownCheckedExes) {
try {
JavaClass exClass = AnalysisContext.currentAnalysisContext().lookupClass(thrownCheckedEx);
addToThrownExsByMethodMap(calledMethodFullName, exClass);
} catch (ClassNotFoundException e) {
AnalysisContext.reportMissingClass(e);
}
}
}
}
}
}
}
private void addToExHandlesToMethodCallsByMethodsMap(String containerMethod, String calledMethod, Collection caughtExes) {
if (exHandlesToMethodCallsByMethodsMap.containsKey(containerMethod)) {
Map> map = exHandlesToMethodCallsByMethodsMap.get(containerMethod);
if (!map.containsKey(calledMethod)) {
map.put(calledMethod, new HashSet<>(caughtExes));
} else {
map.get(calledMethod).addAll(caughtExes);
}
} else {
exHandlesToMethodCallsByMethodsMap.put(containerMethod,
new HashMap>() {
{
put(calledMethod, new HashSet<>(caughtExes));
}
});
}
}
private void addToThrownExsByMethodMap(String containingMethod, JavaClass thrownExClass) {
if (thrownExsByMethodMap.containsKey(containingMethod)) {
thrownExsByMethodMap.get(containingMethod).add(thrownExClass);
} else {
thrownExsByMethodMap.put(containingMethod, new HashSet<>(Collections.singletonList(thrownExClass)));
}
}
/**
* Gives back the fully qualified name (DottedClassName) of the called method complete with the signature.
* Needs to be called from method call opcode.
*
* @return the fully qualified name of the method (dotted) with the signature.
*/
private String getCalledMethodFQN() {
return String.format("%s.%s : %s", getDottedClassConstantOperand(), getNameConstantOperand(),
toDotted(getSigConstantOperand()));
}
private void resetState() {
isFinalClass = false;
isFinalFinalizer = false;
isFirstPass = true;
exHandlesToMethodCallsByMethodsMap.clear();
thrownExsByMethodMap.clear();
}
private void accumulateBug() {
BugInstance bug = new BugInstance(this, "CT_CONSTRUCTOR_THROW", NORMAL_PRIORITY)
.addClassAndMethod(this)
.addSourceLine(this, getPC());
bugAccumulator.accumulateBug(bug, this);
}
private boolean isMethodCall(int seen) {
return seen == Const.INVOKESTATIC
|| seen == Const.INVOKEVIRTUAL
|| seen == Const.INVOKEINTERFACE
|| seen == Const.INVOKESPECIAL;
}
private boolean isConstructor() {
return Const.CONSTRUCTOR_NAME.equals(getMethodName());
}
}