edu.umd.cs.findbugs.detect.FindInconsistentSync2 Maven / Gradle / Ivy
Show all versions of spotbugs Show documentation
/*
* FindBugs - Find bugs in Java programs
* Copyright (C) 2003-2005, University of Maryland
*
* 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.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import org.apache.bcel.Const;
import org.apache.bcel.classfile.ElementValue;
import org.apache.bcel.classfile.Field;
import org.apache.bcel.classfile.JavaClass;
import org.apache.bcel.classfile.Method;
import org.apache.bcel.generic.ConstantPoolGen;
import org.apache.bcel.generic.FieldInstruction;
import org.apache.bcel.generic.IFNONNULL;
import org.apache.bcel.generic.IFNULL;
import org.apache.bcel.generic.INVOKESTATIC;
import org.apache.bcel.generic.Instruction;
import org.apache.bcel.generic.InstructionHandle;
import org.apache.bcel.generic.InstructionList;
import org.apache.bcel.generic.MethodGen;
import org.apache.bcel.generic.ObjectType;
import org.apache.bcel.generic.Type;
import edu.umd.cs.findbugs.BugInstance;
import edu.umd.cs.findbugs.BugReporter;
import edu.umd.cs.findbugs.CallGraph;
import edu.umd.cs.findbugs.CallGraphEdge;
import edu.umd.cs.findbugs.CallGraphNode;
import edu.umd.cs.findbugs.CallSite;
import edu.umd.cs.findbugs.Detector;
import edu.umd.cs.findbugs.IntAnnotation;
import edu.umd.cs.findbugs.Priorities;
import edu.umd.cs.findbugs.SelfCalls;
import edu.umd.cs.findbugs.SourceLineAnnotation;
import edu.umd.cs.findbugs.SystemProperties;
import edu.umd.cs.findbugs.ba.AnalysisContext;
import edu.umd.cs.findbugs.ba.CFG;
import edu.umd.cs.findbugs.ba.CFGBuilderException;
import edu.umd.cs.findbugs.ba.ClassContext;
import edu.umd.cs.findbugs.ba.DataflowAnalysisException;
import edu.umd.cs.findbugs.ba.Hierarchy;
import edu.umd.cs.findbugs.ba.InnerClassAccess;
import edu.umd.cs.findbugs.ba.InnerClassAccessMap;
import edu.umd.cs.findbugs.ba.JCIPAnnotationDatabase;
import edu.umd.cs.findbugs.ba.Location;
import edu.umd.cs.findbugs.ba.LockChecker;
import edu.umd.cs.findbugs.ba.LockSet;
import edu.umd.cs.findbugs.ba.SignatureConverter;
import edu.umd.cs.findbugs.ba.XFactory;
import edu.umd.cs.findbugs.ba.XField;
import edu.umd.cs.findbugs.ba.ch.Subtypes2;
import edu.umd.cs.findbugs.ba.type.TopType;
import edu.umd.cs.findbugs.ba.type.TypeDataflow;
import edu.umd.cs.findbugs.ba.type.TypeFrame;
import edu.umd.cs.findbugs.ba.vna.ValueNumber;
import edu.umd.cs.findbugs.ba.vna.ValueNumberDataflow;
import edu.umd.cs.findbugs.ba.vna.ValueNumberFrame;
import edu.umd.cs.findbugs.classfile.ClassDescriptor;
import edu.umd.cs.findbugs.classfile.DescriptorFactory;
import edu.umd.cs.findbugs.classfile.MethodDescriptor;
import edu.umd.cs.findbugs.props.WarningPropertySet;
import edu.umd.cs.findbugs.util.Util;
/**
* Find instance fields which are sometimes accessed (read or written) with the
* receiver lock held and sometimes without. These are candidates to be data
* races.
*
* @author David Hovemeyer
* @author Bill Pugh
*/
public class FindInconsistentSync2 implements Detector {
private static final boolean DEBUG = SystemProperties.getBoolean("fis.debug");
private static final boolean SYNC_ACCESS = true;
// Boolean.getBoolean("fis.syncAccess");
private static final boolean ADJUST_SUBCLASS_ACCESSES = !SystemProperties.getBoolean("fis.noAdjustSubclass");
private static final boolean EVAL = SystemProperties.getBoolean("fis.eval");
/*
* ----------------------------------------------------------------------
* Tuning parameters
* ----------------------------------------------------------------------
*/
/**
* Minimum percent of unbiased field accesses that must be synchronized in
* order to report a field as inconsistently synchronized. This is meant to
* distinguish incidental synchronization from intentional synchronization.
*/
private static final int MIN_SYNC_PERCENT = SystemProperties.getInt("findbugs.fis.minSyncPercent", 50);
/**
* Bias that writes are given with respect to reads. The idea is that this
* should be above 1.0, because unsynchronized writes are more dangerous
* than unsynchronized reads.
*/
private static final double WRITE_BIAS = Double.parseDouble(SystemProperties.getProperty("findbugs.fis.writeBias", "2.0"));
/**
* Factor which the biased number of unsynchronized accesses is multiplied
* by. I.e., for factor f, if nUnsync is the biased number of
* unsynchronized accesses, and nSync is the biased number of
* synchronized accesses, and
*
*
* f(nUnsync) > nSync
*
*
* then we report a bug. Default value is 2.0, which means that we report a
* bug if more than 1/3 of accesses are unsynchronized.
*
*
* Note that MIN_SYNC_PERCENT
also influences whether we report
* a bug: it specifies the minimum unbiased percentage of synchronized
* accesses.
*/
private static final double UNSYNC_FACTOR = Double.parseDouble(SystemProperties.getProperty("findbugs.fis.unsyncFactor",
"1.6"));
/*
* ----------------------------------------------------------------------
* Helper classes
* ----------------------------------------------------------------------
*/
private static final int UNLOCKED = 0;
private static final int LOCKED = 1;
private static final int READ = 0;
private static final int WRITE = 2;
private static final int NULLCHECK = 4;
private static final int READ_UNLOCKED = READ | UNLOCKED;
private static final int WRITE_UNLOCKED = WRITE | UNLOCKED;
private static final int NULLCHECK_UNLOCKED = NULLCHECK | UNLOCKED;
private static final int READ_LOCKED = READ | LOCKED;
private static final int WRITE_LOCKED = WRITE | LOCKED;
private static final int NULLCHECK_LOCKED = NULLCHECK | LOCKED;
private static class FieldAccess {
final MethodDescriptor methodDescriptor;
final int position;
FieldAccess(MethodDescriptor methodDescriptor, int position) {
this.methodDescriptor = methodDescriptor;
this.position = position;
}
SourceLineAnnotation asSourceLineAnnotation() {
return SourceLineAnnotation.fromVisitedInstruction(methodDescriptor, position);
}
public static Collection asSourceLineAnnotation(Collection c) {
ArrayList result = new ArrayList<>(c.size());
for (FieldAccess f : c) {
result.add(f.asSourceLineAnnotation());
}
return result;
}
}
private static ClassDescriptor servlet = DescriptorFactory.createClassDescriptor("javax/servlet/GenericServlet");
private static ClassDescriptor singleThreadedServlet = DescriptorFactory
.createClassDescriptor("javax/servlet/SingleThreadModel");
public static boolean isServletField(XField field) {
ClassDescriptor classDescriptor = field.getClassDescriptor();
try {
Subtypes2 subtypes2 = AnalysisContext.currentAnalysisContext().getSubtypes2();
if (subtypes2.isSubtype(classDescriptor, servlet) && !subtypes2.isSubtype(classDescriptor, singleThreadedServlet)) {
return true;
}
} catch (ClassNotFoundException e) {
// TODO https://github.com/spotbugs/spotbugs/issues/629
assert true;
}
return classDescriptor.getClassName().endsWith("Servlet");
}
/**
* The access statistics for a field. Stores the number of locked and
* unlocked reads and writes, as well as the number of accesses made with a
* lock held.
*/
private static class FieldStats {
private final int[] countList = new int[6];
private int numLocalLocks = 0;
private int numGetterMethodAccesses = 0;
private List unsyncAccessList = Collections.emptyList();
private List syncAccessList = Collections.emptyList();
boolean interesting = true;
final boolean servletField;
FieldStats(XField field) {
servletField = FindInconsistentSync2.isServletField(field);
}
public void addAccess(int kind) {
countList[kind]++;
}
public int getNumAccesses(int kind) {
return countList[kind];
}
public void addLocalLock() {
numLocalLocks++;
}
public int getNumLocalLocks() {
return numLocalLocks;
}
public void addGetterMethodAccess() {
numGetterMethodAccesses++;
}
public int getNumGetterMethodAccesses() {
return numGetterMethodAccesses;
}
public boolean isInteresting() {
return interesting;
}
public boolean isServletField() {
return servletField;
}
public boolean hasAnySynchronizedAccesses() {
return interesting && !syncAccessList.isEmpty();
}
public void addAccess(MethodDescriptor method, InstructionHandle handle, boolean isLocked) {
if (!interesting) {
return;
}
if (!SYNC_ACCESS && isLocked) {
return;
}
if (!servletField && !isLocked && syncAccessList.size() == 0 && unsyncAccessList.size() > 6) {
interesting = false;
syncAccessList = null;
unsyncAccessList = null;
return;
}
FieldAccess fa = new FieldAccess(method, handle.getPosition());
if (isLocked) {
syncAccessList = Util.addTo(syncAccessList, fa);
} else {
unsyncAccessList = Util.addTo(unsyncAccessList, fa);
}
}
public Iterator unsyncAccessIterator() {
if (!interesting) {
throw new IllegalStateException("Not interesting");
}
return FieldAccess.asSourceLineAnnotation(unsyncAccessList).iterator();
}
public Iterator syncAccessIterator() {
if (!interesting) {
throw new IllegalStateException("Not interesting");
}
return FieldAccess.asSourceLineAnnotation(syncAccessList).iterator();
}
}
/*
* ----------------------------------------------------------------------
* Fields
* ----------------------------------------------------------------------
*/
private final BugReporter bugReporter;
private final Map statMap = new HashMap<>();
/*
* ----------------------------------------------------------------------
* Public methods
* ----------------------------------------------------------------------
*/
public FindInconsistentSync2(BugReporter bugReporter) {
this.bugReporter = bugReporter;
}
@Override
public void visitClassContext(ClassContext classContext) {
JavaClass javaClass = classContext.getJavaClass();
if (DEBUG) {
System.out.println("******** Analyzing class " + javaClass.getClassName());
}
// Build self-call graph
SelfCalls selfCalls = new SelfCalls(classContext) {
@Override
public boolean wantCallsFor(Method method) {
return !method.isPublic();
}
};
Set lockedMethodSet;
// Set publicReachableMethods;
Set allMethods = new HashSet<>(Arrays.asList(javaClass.getMethods()));
try {
selfCalls.execute();
CallGraph callGraph = selfCalls.getCallGraph();
if (DEBUG) {
System.out.println("Call graph (not unlocked methods): " + callGraph.getNumVertices() + " nodes, "
+ callGraph.getNumEdges() + " edges");
}
// Find call edges that are obviously locked
Set obviouslyLockedSites = findObviouslyLockedCallSites(classContext, selfCalls);
lockedMethodSet = findNotUnlockedMethods(classContext, selfCalls, obviouslyLockedSites);
lockedMethodSet.retainAll(findLockedMethods(classContext, selfCalls, obviouslyLockedSites));
// publicReachableMethods = findPublicReachableMethods(classContext,
// selfCalls);
} catch (CFGBuilderException e) {
bugReporter.logError("Error finding locked call sites", e);
return;
} catch (DataflowAnalysisException e) {
bugReporter.logError("Error finding locked call sites", e);
return;
}
for (Method method : allMethods) {
if (DEBUG) {
System.out.println("******** considering method " + method.getName());
}
if (classContext.getMethodGen(method) == null) {
continue;
}
if (method.getName().startsWith("access$")) {
// Ignore inner class access methods;
// we will treat calls to them as field accesses
continue;
}
String name = method.getName();
boolean inConstructor = Const.CONSTRUCTOR_NAME.equals(name) || Const.STATIC_INITIALIZER_NAME.equals(name)
|| "readObject".equals(name) || "clone".equals(name) || "close".equals(name)
|| "finalize".equals(name);
if (inConstructor) {
continue;
}
if (DEBUG) {
System.out.println("******** Analyzing method " + method.getName());
}
try {
analyzeMethod(classContext, method, lockedMethodSet);
} catch (CFGBuilderException e) {
bugReporter.logError("Error analyzing method", e);
} catch (DataflowAnalysisException e) {
bugReporter.logError("Error analyzing method", e);
}
}
for (Field f : javaClass.getFields()) {
if (f.isPrivate()) {
XField xf = XFactory.getExactXField(classContext.getClassDescriptor().getClassName(), f);
FieldStats stats = statMap.get(xf);
if (stats == null) {
continue;
}
if (!stats.isServletField() && !stats.hasAnySynchronizedAccesses()) {
statMap.remove(xf);
}
}
}
}
@Override
public void report() {
if (statMap.isEmpty()) {
return;
}
JCIPAnnotationDatabase jcipAnotationDatabase = AnalysisContext.currentAnalysisContext().getJCIPAnnotationDatabase();
for (Entry entry : statMap.entrySet()) {
XField xfield = entry.getKey();
FieldStats stats = entry.getValue();
if (!stats.isInteresting()) {
continue;
}
boolean notThreadSafe = jcipAnotationDatabase.hasClassAnnotation(xfield.getClassName(), "NotThreadSafe");
if (notThreadSafe) {
continue;
}
ElementValue guardedByValue = jcipAnotationDatabase.getFieldAnnotation(xfield, "GuardedBy");
boolean guardedByThis;
if (guardedByValue != null) {
guardedByThis = "this".equals(guardedByValue.stringifyValue());
} else {
guardedByThis = false;
}
boolean threadSafe = jcipAnotationDatabase.hasClassAnnotation(xfield.getClassName(), "ThreadSafe");
WarningPropertySet propertySet = new WarningPropertySet<>();
int numReadUnlocked = stats.getNumAccesses(READ_UNLOCKED);
int numWriteUnlocked = stats.getNumAccesses(WRITE_UNLOCKED);
int numNullCheckUnlocked = stats.getNumAccesses(NULLCHECK_UNLOCKED);
int numReadLocked = stats.getNumAccesses(READ_LOCKED);
int numWriteLocked = stats.getNumAccesses(WRITE_LOCKED);
int numNullCheckLocked = stats.getNumAccesses(NULLCHECK_LOCKED);
int extra = 0;
if (numWriteUnlocked > 0) {
extra = numNullCheckLocked;
}
int locked = numReadLocked + numWriteLocked + numNullCheckLocked;
int biasedLocked = numReadLocked + (int) (WRITE_BIAS * (numWriteLocked + numNullCheckLocked + extra));
int unlocked = numReadUnlocked + numWriteUnlocked + numNullCheckUnlocked;
int biasedUnlocked = numReadUnlocked + (int) (WRITE_BIAS * (numWriteUnlocked));
// int writes = numWriteLocked + numWriteUnlocked;
if (unlocked == 0) {
continue;
// propertySet.addProperty(InconsistentSyncWarningProperty.NEVER_UNLOCKED);
}
if (guardedByThis) {
propertySet.addProperty(InconsistentSyncWarningProperty.ANNOTATED_AS_GUARDED_BY_THIS);
}
if (threadSafe) {
propertySet.addProperty(InconsistentSyncWarningProperty.ANNOTATED_AS_THREAD_SAFE);
}
if (!guardedByThis && locked == 0 && !stats.isServletField()) {
continue;
// propertySet.addProperty(InconsistentSyncWarningProperty.NEVER_LOCKED);
}
if (stats.isServletField() && numWriteLocked == 0 && numWriteUnlocked == 0) {
continue;
}
if (DEBUG) {
System.out.println("IS2: " + xfield);
if (guardedByThis) {
System.out.println("Guarded by this");
}
System.out.println(" RL: " + numReadLocked);
System.out.println(" WL: " + numWriteLocked);
System.out.println(" NL: " + numNullCheckLocked);
System.out.println(" RU: " + numReadUnlocked);
System.out.println(" WU: " + numWriteUnlocked);
System.out.println(" NU: " + numNullCheckUnlocked);
}
if (!EVAL && numReadUnlocked > 0 && ((int) (UNSYNC_FACTOR * (biasedUnlocked - 1))) > biasedLocked
&& !stats.isServletField()) {
// continue;
propertySet.addProperty(InconsistentSyncWarningProperty.MANY_BIASED_UNLOCKED);
}
// NOTE: we ignore access to public, volatile, and final fields
if (numWriteUnlocked + numWriteLocked == 0) {
// No writes outside of constructor
if (DEBUG) {
System.out.println(" No writes outside of constructor");
}
propertySet.addProperty(InconsistentSyncWarningProperty.NEVER_WRITTEN);
// continue;
}
if (numReadUnlocked + numReadLocked == 0) {
// No reads outside of constructor
if (DEBUG) {
System.out.println(" No reads outside of constructor");
}
// continue;
propertySet.addProperty(InconsistentSyncWarningProperty.NEVER_READ);
}
if (stats.getNumLocalLocks() == 0) {
if (DEBUG) {
System.out.println(" No local locks");
}
// continue;
propertySet.addProperty(InconsistentSyncWarningProperty.NO_LOCAL_LOCKS);
}
int freq, printFreq;
if (locked + unlocked > 0) {
freq = (100 * locked) / (locked + unlocked);
printFreq = (100 * locked) / (locked + unlocked + numNullCheckUnlocked);
} else {
printFreq = freq = 0;
}
if (freq < MIN_SYNC_PERCENT) {
// continue;
propertySet.addProperty(InconsistentSyncWarningProperty.BELOW_MIN_SYNC_PERCENT);
}
if (DEBUG) {
System.out.println(" Sync %: " + freq);
}
if (stats.getNumGetterMethodAccesses() >= unlocked) {
// Unlocked accesses are only in getter method(s).
propertySet.addProperty(InconsistentSyncWarningProperty.ONLY_UNSYNC_IN_GETTERS);
}
// At this point, we report the field as being inconsistently
// synchronized
if (stats.isServletField()) {
propertySet.addProperty(InconsistentSyncWarningProperty.MUTABLE_SERVLET_FIELD);
}
BugInstance bugInstance;
if (stats.isServletField()) {
bugInstance = new BugInstance(this, "MSF_MUTABLE_SERVLET_FIELD", Priorities.NORMAL_PRIORITY).addClass(
xfield.getClassName()).addField(xfield);
} else {
bugInstance = new BugInstance(this, guardedByThis ? "IS_FIELD_NOT_GUARDED" : "IS2_INCONSISTENT_SYNC",
Priorities.NORMAL_PRIORITY).addClass(xfield.getClassName()).addField(xfield).addInt(printFreq)
.describe(IntAnnotation.INT_SYNC_PERCENT);
}
propertySet.decorateBugInstance(bugInstance);
// Add source lines for unsynchronized accesses
for (Iterator j = stats.unsyncAccessIterator(); j.hasNext();) {
SourceLineAnnotation accessSourceLine = j.next();
bugInstance.addSourceLine(accessSourceLine).describe("SOURCE_LINE_UNSYNC_ACCESS");
}
if (SYNC_ACCESS) {
// Add source line for synchronized accesses;
// useful for figuring out what the detector is doing
for (Iterator j = stats.syncAccessIterator(); j.hasNext();) {
SourceLineAnnotation accessSourceLine = j.next();
bugInstance.addSourceLine(accessSourceLine).describe("SOURCE_LINE_SYNC_ACCESS");
}
}
if (EVAL) {
bugInstance.addInt(biasedLocked).describe("INT_BIASED_LOCKED");
bugInstance.addInt(biasedUnlocked).describe("INT_BIASED_UNLOCKED");
}
bugReporter.reportBug(bugInstance);
}
}
/*
* ----------------------------------------------------------------------
* Implementation
* ----------------------------------------------------------------------
*/
private static boolean isConstructor(String methodName) {
return Const.CONSTRUCTOR_NAME.equals(methodName) || Const.STATIC_INITIALIZER_NAME.equals(methodName) || "readObject".equals(methodName)
|| "clone".equals(methodName) || "close".equals(methodName) || "writeObject".equals(methodName)
|| "toString".equals(methodName) || "init".equals(methodName) || "initialize".equals(methodName)
|| "dispose".equals(methodName) || "finalize".equals(methodName) || "this".equals(methodName)
|| "_jspInit".equals(methodName) || "_jspDestroy".equals(methodName);
}
private void analyzeMethod(ClassContext classContext, Method method, Set lockedMethodSet) throws CFGBuilderException,
DataflowAnalysisException {
InnerClassAccessMap icam = AnalysisContext.currentAnalysisContext().getInnerClassAccessMap();
ConstantPoolGen cpg = classContext.getConstantPoolGen();
MethodGen methodGen = classContext.getMethodGen(method);
if (methodGen == null) {
return;
}
CFG cfg = classContext.getCFG(method);
LockChecker lockChecker = classContext.getLockChecker(method);
ValueNumberDataflow vnaDataflow = classContext.getValueNumberDataflow(method);
boolean isGetterMethod = isGetterMethod(classContext, method);
MethodDescriptor methodDescriptor = DescriptorFactory.instance().getMethodDescriptor(classContext.getJavaClass(), method);
if (DEBUG) {
System.out.println("**** Analyzing method " + SignatureConverter.convertMethodSignature(methodGen));
}
for (Iterator i = cfg.locationIterator(); i.hasNext();) {
Location location = i.next();
try {
Instruction ins = location.getHandle().getInstruction();
XField xfield = null;
boolean isWrite = false;
boolean isLocal = false;
boolean isNullCheck = false;
if (ins instanceof FieldInstruction) {
InstructionHandle n = location.getHandle().getNext();
isNullCheck = n.getInstruction() instanceof IFNONNULL || n.getInstruction() instanceof IFNULL;
if (DEBUG && isNullCheck) {
System.out.println("is null check");
}
FieldInstruction fins = (FieldInstruction) ins;
xfield = Hierarchy.findXField(fins, cpg);
if (xfield == null) {
continue;
}
isWrite = ins.getOpcode() == Const.PUTFIELD;
isLocal = fins.getClassName(cpg).equals(classContext.getJavaClass().getClassName());
if (DEBUG) {
System.out.println("Handling field access: " + location.getHandle() + " (frame="
+ vnaDataflow.getFactAtLocation(location) + ") :" + n);
}
} else if (ins instanceof INVOKESTATIC) {
INVOKESTATIC inv = (INVOKESTATIC) ins;
InnerClassAccess access = icam.getInnerClassAccess(inv, cpg);
if (access != null && access.getMethodSignature().equals(inv.getSignature(cpg))) {
xfield = access.getField();
isWrite = !access.isLoad();
isLocal = false;
if (DEBUG) {
System.out.println("Handling inner class access: " + location.getHandle() + " (frame="
+ vnaDataflow.getFactAtLocation(location) + ")");
}
}
}
if (xfield == null) {
continue;
}
// We only care about mutable nonvolatile nonpublic instance
// fields.
if (xfield.isStatic() || xfield.isPublic() || xfield.isVolatile() || xfield.isFinal()) {
continue;
}
// The value number frame could be invalid if the basic
// block became unreachable due to edge pruning (dead code).
ValueNumberFrame frame = vnaDataflow.getFactAtLocation(location);
if (!frame.isValid()) {
continue;
}
// Get lock set and instance value
ValueNumber thisValue = !method.isStatic() ? vnaDataflow.getAnalysis().getThisValue() : null;
LockSet lockSet = lockChecker.getFactAtLocation(location);
InstructionHandle handle = location.getHandle();
ValueNumber instance = frame.getInstance(handle.getInstruction(), cpg);
if (DEBUG) {
System.out.println("Lock set: " + lockSet);
System.out.println("value number: " + instance.getNumber());
System.out.println("Lock count: " + lockSet.getLockCount(instance.getNumber()));
}
// Is the instance locked?
// We consider the access to be locked if either
// - the object is explicitly locked, or
// - the field is accessed through the "this" reference,
// and the method is in the locked method set, or
// - any value returned by a called method is locked;
// the (conservative) assumption is that the return lock object
// is correct for synchronizing the access
boolean isExplicitlyLocked = lockSet.getLockCount(instance.getNumber()) > 0;
boolean isAccessedThroughThis = thisValue != null && thisValue.equals(instance);
boolean isLocked = isExplicitlyLocked
|| ((isConstructor(method.getName()) || lockedMethodSet.contains(method)) && isAccessedThroughThis)
|| lockSet.containsReturnValue(vnaDataflow.getAnalysis().getFactory());
// Adjust the field so its class name is the same
// as the type of reference it is accessed through.
// This helps fix false positives produced when a
// threadsafe class is extended by a subclass that
// doesn't care about thread safety.
if (ADJUST_SUBCLASS_ACCESSES) {
// Find the type of the object instance
TypeDataflow typeDataflow = classContext.getTypeDataflow(method);
TypeFrame typeFrame = typeDataflow.getFactAtLocation(location);
if (!typeFrame.isValid()) {
continue;
}
Type instanceType = typeFrame.getInstance(handle.getInstruction(), cpg);
if (instanceType instanceof TopType) {
if (DEBUG) {
System.out.println("Freaky: typeFrame is " + typeFrame);
}
continue;
}
// Note: instance type can be Null,
// in which case we won't adjust the field type.
if (instanceType != TypeFrame.getNullType() && instanceType != TypeFrame.getBottomType()) {
if (!(instanceType instanceof ObjectType)) {
throw new DataflowAnalysisException("Field accessed through non-object reference " + instanceType,
methodGen, handle);
}
ObjectType objType = (ObjectType) instanceType;
// If instance class name is not the same as that of the
// field,
// make it so
String instanceClassName = objType.getClassName();
if (!instanceClassName.equals(xfield.getClassName())) {
xfield = XFactory.getExactXField(instanceClassName, xfield.getName(), xfield.getSignature(),
xfield.isStatic());
}
}
}
int kind = 0;
kind |= isLocked ? LOCKED : UNLOCKED;
kind |= isWrite ? WRITE : isNullCheck ? NULLCHECK : READ;
// if (isLocked || !isConstructor(method.getName())) {
if (DEBUG) {
System.out.println("IS2:\t" + SignatureConverter.convertMethodSignature(methodGen) + "\t" + xfield + "\t"
+ ((isWrite ? "W" : "R") + "/" + (isLocked ? "L" : "U")));
}
if (!isLocked && methodDescriptor.getClassDescriptor().isAnonymousClass()) {
continue;
}
FieldStats stats = getStats(xfield);
// Don't count a contructor's synchronized access
// toward the field statistics because it's
// trivially true and doesn't really represent the
// programmer's intention
if (!(isLocked && isConstructor(method.getName()))) {
stats.addAccess(kind);
}
if (isExplicitlyLocked && isLocal) {
stats.addLocalLock();
}
if (isGetterMethod && !isLocked) {
stats.addGetterMethodAccess();
}
stats.addAccess(methodDescriptor, handle, isLocked);
// }
} catch (ClassNotFoundException e) {
bugReporter.reportMissingClass(e);
}
}
}
/**
* Determine whether or not the given method is a getter method. I.e.,
* if it just returns the value of an instance field.
*
* @param classContext
* the ClassContext for the class containing the method
* @param method
* the method
*/
public static boolean isGetterMethod(ClassContext classContext, Method method) {
MethodGen methodGen = classContext.getMethodGen(method);
if (methodGen == null) {
return false;
}
InstructionList il = methodGen.getInstructionList();
// System.out.println("Checking getter method: " + method.getName());
if (il.getLength() > 60) {
return false;
}
int count = 0;
for (InstructionHandle ih : il) {
switch (ih.getInstruction().getOpcode()) {
case Const.GETFIELD:
count++;
if (count > 1) {
return false;
}
break;
case Const.PUTFIELD:
case Const.BALOAD:
case Const.CALOAD:
case Const.DALOAD:
case Const.FALOAD:
case Const.IALOAD:
case Const.LALOAD:
case Const.SALOAD:
case Const.AALOAD:
case Const.BASTORE:
case Const.CASTORE:
case Const.DASTORE:
case Const.FASTORE:
case Const.IASTORE:
case Const.LASTORE:
case Const.SASTORE:
case Const.AASTORE:
case Const.PUTSTATIC:
return false;
case Const.INVOKESTATIC:
case Const.INVOKEVIRTUAL:
case Const.INVOKEINTERFACE:
case Const.INVOKESPECIAL:
case Const.GETSTATIC:
// no-op
}
}
// System.out.println("Found getter method: " + method.getName());
return true;
}
/**
* Get the access statistics for given field.
*/
private FieldStats getStats(XField field) {
FieldStats stats = statMap.get(field);
if (stats == null) {
stats = new FieldStats(field);
statMap.put(field, stats);
}
return stats;
}
/**
* Find methods that appear to never be called from an unlocked context We
* assume that nonpublic methods will only be called from within the class,
* which is not really a valid assumption.
*/
private static Set findNotUnlockedMethods(ClassContext classContext, SelfCalls selfCalls, Set obviouslyLockedSites) {
JavaClass javaClass = classContext.getJavaClass();
Method[] methodList = javaClass.getMethods();
CallGraph callGraph = selfCalls.getCallGraph();
// Initially, assume no methods are called from an
// unlocked context
Set lockedMethodSet = new HashSet<>(Arrays.asList(methodList));
// Assume all public methods are called from
// unlocked context
for (Method method : methodList) {
if (method.isPublic() && !isConstructor(method.getName())) {
lockedMethodSet.remove(method);
}
}
// Explore the self-call graph to find nonpublic methods
// that can be called from an unlocked context.
boolean change;
do {
change = false;
for (Iterator i = callGraph.edgeIterator(); i.hasNext();) {
CallGraphEdge edge = i.next();
CallSite callSite = edge.getCallSite();
// Ignore obviously locked edges
if (obviouslyLockedSites.contains(callSite)) {
continue;
}
// If the calling method is locked, ignore the edge
if (lockedMethodSet.contains(callSite.getMethod())) {
continue;
}
// Calling method is unlocked, so the called method
// is also unlocked.
CallGraphNode target = edge.getTarget();
if (lockedMethodSet.remove(target.getMethod())) {
change = true;
}
}
} while (change);
if (DEBUG) {
System.out.println("Apparently not unlocked methods:");
for (Method method : lockedMethodSet) {
System.out.println("\t" + method.getName());
}
}
// We assume that any methods left in the locked set
// are called only from a locked context.
return lockedMethodSet;
}
/**
* Find methods that appear to always be called from a locked context. We
* assume that nonpublic methods will only be called from within the class,
* which is not really a valid assumption.
*/
private static Set findLockedMethods(ClassContext classContext, SelfCalls selfCalls, Set obviouslyLockedSites) {
JavaClass javaClass = classContext.getJavaClass();
Method[] methodList = javaClass.getMethods();
CallGraph callGraph = selfCalls.getCallGraph();
// Initially, assume all methods are locked
Set lockedMethodSet = new HashSet<>();
// Assume all public methods are unlocked
for (Method method : methodList) {
if (method.isSynchronized()) {
lockedMethodSet.add(method);
}
}
// Explore the self-call graph to find nonpublic methods
// that can be called from an unlocked context.
boolean change;
do {
change = false;
for (Iterator i = callGraph.edgeIterator(); i.hasNext();) {
CallGraphEdge edge = i.next();
CallSite callSite = edge.getCallSite();
if (obviouslyLockedSites.contains(callSite) || lockedMethodSet.contains(callSite.getMethod())) {
// Calling method is locked, so the called method
// is also locked.
CallGraphNode target = edge.getTarget();
if (lockedMethodSet.add(target.getMethod())) {
change = true;
}
}
}
} while (change);
if (DEBUG) {
System.out.println("Apparently locked methods:");
for (Method method : lockedMethodSet) {
System.out.println("\t" + method.getName());
}
}
// We assume that any methods left in the locked set
// are called only from a locked context.
return lockedMethodSet;
}
/**
* Find methods that do not appear to be reachable from public methods. Such
* methods will not be analyzed.
*/
/*
* private Set findPublicReachableMethods(ClassContext classContext,
* SelfCalls selfCalls) throws CFGBuilderException,
* DataflowAnalysisException {
*
* JavaClass javaClass = classContext.getJavaClass(); Method[] methodList =
* javaClass.getMethods();
*
* CallGraph callGraph = selfCalls.getCallGraph();
*
* // Initially, assume all methods are locked Set
* publicReachableMethodSet = new HashSet();
*
* // Assume all public methods are unlocked for (Method method :
* methodList) { if (method.isPublic() && !isConstructor(method.getName()))
* { publicReachableMethodSet.add(method); } }
*
* // Explore the self-call graph to find nonpublic methods // that can be
* called from an unlocked context. boolean change; do { change = false;
*
* for (Iterator i = callGraph.edgeIterator(); i.hasNext();)
* { CallGraphEdge edge = i.next(); CallSite callSite = edge.getCallSite();
*
* // Ignore obviously locked edges // If the calling method is locked,
* ignore the edge if
* (publicReachableMethodSet.contains(callSite.getMethod())) { // Calling
* method is reachable, so the called method // is also reachable.
* CallGraphNode target = edge.getTarget(); if
* (publicReachableMethodSet.add(target.getMethod())) change = true; } } }
* while (change);
*
* if (DEBUG) { System.out.println(
* "Methods apparently reachable from public non-constructor methods:"); for
* (Method method : publicReachableMethodSet) { System.out.println("\t" +
* method.getName()); } }
*
* return publicReachableMethodSet; }
*/
/**
* Find all self-call sites that are obviously locked.
*/
private static Set findObviouslyLockedCallSites(ClassContext classContext, SelfCalls selfCalls)
throws CFGBuilderException, DataflowAnalysisException {
ConstantPoolGen cpg = classContext.getConstantPoolGen();
// Find all obviously locked call sites
Set obviouslyLockedSites = new HashSet<>();
for (Iterator i = selfCalls.callSiteIterator(); i.hasNext();) {
CallSite callSite = i.next();
Method method = callSite.getMethod();
Location location = callSite.getLocation();
InstructionHandle handle = location.getHandle();
// Only instance method calls qualify as candidates for
// "obviously locked"
Instruction ins = handle.getInstruction();
if (ins.getOpcode() == Const.INVOKESTATIC) {
continue;
}
// Get lock set for site
LockChecker lockChecker = classContext.getLockChecker(method);
LockSet lockSet = lockChecker.getFactAtLocation(location);
// Get value number frame for site
ValueNumberDataflow vnaDataflow = classContext.getValueNumberDataflow(method);
ValueNumberFrame frame = vnaDataflow.getFactAtLocation(location);
// NOTE: if the CFG on which the value number analysis was performed
// was pruned, there may be unreachable instructions. Therefore,
// we can't assume the frame is valid.
if (!frame.isValid()) {
continue;
}
// Find the ValueNumber of the receiver object
int numConsumed = ins.consumeStack(cpg);
MethodGen methodGen = classContext.getMethodGen(method);
assert methodGen != null;
if (numConsumed == Const.UNPREDICTABLE) {
throw new DataflowAnalysisException("Unpredictable stack consumption", methodGen, handle);
}
// if (DEBUG) System.out.println("Getting receiver for frame: " +
// frame);
ValueNumber instance = frame.getStackValue(numConsumed - 1);
// Is the instance locked?
int lockCount = lockSet.getLockCount(instance.getNumber());
if (lockCount > 0) {
// This is a locked call site
obviouslyLockedSites.add(callSite);
}
}
return obviouslyLockedSites;
}
}