/** * Copyright (C) 2013 Rohan Padhye * * 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 program. If not, see <http://www.gnu.org/licenses/>. * */ package vasco.callgraph; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import soot.ArrayType; import soot.Local; import soot.RefLikeType; import soot.RefType; import soot.Scene; import soot.SootClass; import soot.SootField; import soot.Type; import soot.jimple.AnyNewExpr; import soot.jimple.ClassConstant; import soot.jimple.Constant; import soot.jimple.NewArrayExpr; import soot.jimple.NewExpr; import soot.jimple.NewMultiArrayExpr; import soot.jimple.NullConstant; import soot.jimple.StringConstant; import soot.jimple.internal.JNewExpr; import soot.jimple.internal.JimpleLocal; /** * A data flow value representation for points-to analysis using allocation sites. * * <p>The points-to graph contains two types of edges: (1) from root variables * ({@link soot.Local Local}) to objects represented by allocation * sites ({@link soot.jimple.AnyNewExpr AnyNewExpr}), * and (2) from objects to objects along fields ({@link soot.SootField SootField}).</p> * * <p>Special artificial locals are used for string constants, class constants, * return values, etc. and artificial sites are used for summary nodes. For arrays * an artificial field is used to represent element access.</p> * * @author Rohan Padhye */ public class PointsToGraph { public static final SootField ARRAY_FIELD = new SootField("[]", Scene.v().getObjectType()); public static final Local GLOBAL_LOCAL = new JimpleLocal("@global", Scene.v().getObjectType()); public static final Local RETURN_LOCAL = new JimpleLocal("@return", Scene.v().getObjectType()); public static final Local STICKY_LOCAL = new JimpleLocal("@params", Scene.v().getObjectType()); public static final Constant STRING_CONST = StringConstant.v("GLOBAL STRING CONSTANT"); public static final Constant CLASS_CONST = ClassConstant.v("java/lang/Object"); public static final NewExpr STRING_SITE = new JNewExpr(Scene.v().getRefType("java.lang.String")); public static final NewExpr CLASS_SITE = new JNewExpr(Scene.v().getRefType("java.lang.Class")); public static final NewExpr SUMMARY_NODE = new JNewExpr(null); public static final NewExpr GLOBAL_SITE = new JNewExpr(Scene.v().getObjectType()); protected Map<Local,Set<AnyNewExpr>> roots; protected Map<AnyNewExpr,Map<SootField,Set<AnyNewExpr>>> heap; /** * Constructs a new empty points-to graph. */ public PointsToGraph() { roots = new HashMap<Local,Set<AnyNewExpr>>(); heap = new HashMap<AnyNewExpr,Map<SootField,Set<AnyNewExpr>>>(); } /** * Constructs a copy of the given points-to graph. * * @param other the points-to graph to copy */ public PointsToGraph(PointsToGraph other) { // As the data inside the top-level maps are immutable, just a shallow // copy will suffice this.roots = new HashMap<Local,Set<AnyNewExpr>>(other.roots); this.heap = new HashMap<AnyNewExpr,Map<SootField,Set<AnyNewExpr>>>(other.heap); } /** * Adds an edge between two sites with a given field. */ @SuppressWarnings("unused") private void addEdge(AnyNewExpr n1, SootField field, AnyNewExpr n2) { // No edge from null assert_tmp (n1 != null && n2 != null && field != null); // Ensure nodes exist in the heap ensureNode(n1); ensureNode(n2); // Add the field edge to a copy of the current edges. Map<SootField,Set<AnyNewExpr>> oldEdges = heap.get(n1); Map<SootField,Set<AnyNewExpr>> newEdges = new HashMap<SootField,Set<AnyNewExpr>>(oldEdges); Set<AnyNewExpr> oldTargets = oldEdges.containsKey(field) ? oldEdges.get(field) : new HashSet<AnyNewExpr>(); Set<AnyNewExpr> newTargets = new HashSet<AnyNewExpr>(oldTargets); boolean change = newTargets.add(n2); if (change) { newEdges.put(field, Collections.unmodifiableSet(newTargets)); heap.put(n1, Collections.unmodifiableMap(newEdges)); } } /** * Adds an edge between a variable and a node. */ @SuppressWarnings("unused") private void addEdge(Local var, AnyNewExpr node) { assert_tmp(var != null && node != null); // Ensure entry exists for field edges. ensureNode(node); // Add the root variable edge. Set<AnyNewExpr> oldPointees = roots.containsKey(var) ? roots.get(var) : new HashSet<AnyNewExpr>(); Set<AnyNewExpr> newPointees = new HashSet<AnyNewExpr>(oldPointees); boolean change = newPointees.add(node); if (change) { roots.put(var, Collections.unmodifiableSet(newPointees)); } } /** * Returns <tt>true</tt> only if there is an edge * from the given root variable to the given heap node. */ public boolean containsEdge(Local var, AnyNewExpr node) { return roots.get(var).contains(node); } /** * Assigns a root variable to a root variable. */ public void assign(Local lhs, Local rhs) { // Find whatever the RHS was pointing to. Set<AnyNewExpr> rhsTargets = roots.get(rhs); // We will fill this up with correctly typed targets Set<AnyNewExpr> lhsTargets = new HashSet<AnyNewExpr>(); Type lhsType = lhs.getType(); if (lhsType instanceof ArrayType) { lhsType = ((ArrayType) lhsType).baseType; } if (rhsTargets != null) { // Handle references and arrays separately if (lhsType instanceof RefType) { SootClass toClass = ((RefType) lhsType).getSootClass(); for (AnyNewExpr rhsTarget : rhsTargets) { // Handle only instance objects if (rhsTarget instanceof AnyNewExpr) { // Do not type-check for summary nodes and when the LHS is java.lang.Object if (rhsTarget == SUMMARY_NODE || lhsType.equals(Scene.v().getObjectType())) { // Add by default lhsTargets.add(rhsTarget); continue; } Type rhsTargetType = rhsTarget.getType(); if (rhsTargetType instanceof ArrayType) { rhsTargetType = ((ArrayType) rhsTargetType).baseType; } // If the type (or base type) is reftype, then type-check if (rhsTargetType instanceof RefType) { SootClass fromClass = ((RefType) rhsTargetType).getSootClass(); if (PointsToGraph.canCast(fromClass, toClass)) { // Yes, add this target lhsTargets.add(rhsTarget); } } else { // For non-ref base types (e.g. char[]), just add lhsTargets.add(rhsTarget); } } } } else if (lhsType instanceof ArrayType) { // We are not so fickle about arrays lhsTargets.addAll(rhsTargets); } } // Add the targets to the LHS edges. roots.put(lhs, Collections.unmodifiableSet(lhsTargets)); // Ensure reachability gc(); } /** * Assigns a constant to a root variable. */ public void assignConstant(Local lhs, Constant rhs) { // Get the allocation site of this constant NewExpr newExpr = constantNewExpr(rhs); // If it was a null constant, assign null, otherwise assign alloc site if (newExpr == null) { assign(lhs, null); } else { assignNew(lhs, newExpr); } } /** * Assigns the sticky local to a parameter. */ public void assignSticky(Local sticky, Local parameter) { Set<AnyNewExpr> rhsTargets = roots.get(parameter); Set<AnyNewExpr> lhsTargets = new HashSet<AnyNewExpr>(roots.get(sticky)); boolean change = lhsTargets.addAll(rhsTargets); if (change) { roots.put(sticky, Collections.unmodifiableSet(lhsTargets)); } } /** * Assigns a root variable to a new object at a given allocation site. */ public void assignNew(Local lhs, AnyNewExpr allocSite) { // If creating a new string or class, re-use the constant site if (allocSite != SUMMARY_NODE) { if (allocSite.getType().equals(STRING_CONST.getType())) { allocSite = STRING_SITE; } else if (allocSite.getType().equals(CLASS_CONST.getType())) { allocSite = CLASS_SITE; } } // We do not handle multi-dimensional arrays in this version if (allocSite instanceof NewMultiArrayExpr) { allocSite = SUMMARY_NODE; } // Create this node in the heap, if it doesn't already exist newNode(allocSite, false); // Assign LHS to the new node Set<AnyNewExpr> target = new HashSet<AnyNewExpr>(); target.add(allocSite); roots.put(lhs, Collections.unmodifiableSet(target)); // Ensure reachability. gc(); } /** * Assigns a set of targets to a root variable. */ void assignGlobals(Local lhs, Set<AnyNewExpr> targets) { // Ensure all nodes exist in the heap for (AnyNewExpr allocSite : targets) { newNode(allocSite, true); } // Assign LHS to all these nodes roots.put(lhs, Collections.unmodifiableSet(targets)); // Ensure reachability. gc(); } /** * Assigns a root variable to the summary node. */ public void assignSummary(Local lhs) { assignNew(lhs, SUMMARY_NODE); } /** * Determines whether an object of one class can be cast to another class. * * @param fromClass the source type * @param toClass the target type * @return <tt>true</tt> if and only if <tt>fromClass</tt> is a sub-type of (or implements) <tt>toClass</tt> */ public static boolean canCast(SootClass fromClass, SootClass toClass) { // Handle classes and interfaces differently if (toClass.isInterface()) { // For interfaces, the fromClass (or one of its super-classes) must // implement fromClass if (fromClass.implementsInterface(toClass.toString())) { return true; } if (fromClass.getInterfaceCount() > 0) { // Check sub interfaces for (SootClass subInterface: fromClass.getInterfaces()) { if (canCast(subInterface, toClass)) { return true; } } } if (fromClass.hasSuperclass()) { return canCast(fromClass.getSuperclass(), toClass); } else { return false; } } else { // For classes, the fromClass (or one of its super-classes) and // toClass have to be same. if (fromClass.equals(toClass)) { return true; } else if (fromClass.hasSuperclass()) { return canCast(fromClass.getSuperclass(), toClass); } else { return false; } } } /** * Creates a new node for a constant. */ private NewExpr constantNewExpr(Constant constant) { if (constant instanceof StringConstant) { return STRING_SITE; } else if (constant instanceof ClassConstant) { return CLASS_SITE; } else if (constant instanceof NullConstant) { return null; } else { throw new RuntimeException(constant.toString()); } } /** * Ensures the given node is in the heap. */ private void ensureNode(AnyNewExpr node) { // WARNING: No fields are added if this is used! if (node != null && !heap.containsKey(node)) heap.put(node, Collections.unmodifiableMap(new HashMap<SootField,Set<AnyNewExpr>>())); } /** * {@inheritDoc} */ @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (!(obj instanceof PointsToGraph)) return false; PointsToGraph other = (PointsToGraph) obj; if (heap == null) { if (other.heap != null) return false; } else if (!heap.equals(other.heap)) return false; if (roots == null) { if (other.roots != null) return false; } else if (!roots.equals(other.roots)) return false; return true; } /** * Removes all unreachable nodes from the edge sets. */ public void gc() { // Maintain a work-list of (reachable) nodes to process. LinkedList<AnyNewExpr> worklist = new LinkedList<AnyNewExpr>(); // Add all the nodes pointed-to by root variables to the work-list. for (Set<AnyNewExpr> nodes : roots.values()) { worklist.addAll(nodes); } // Start with an empty field map (save the old one!) Map<AnyNewExpr,Map<SootField,Set<AnyNewExpr>>> oldHeap = this.heap; Map<AnyNewExpr,Map<SootField,Set<AnyNewExpr>>> newHeap = new HashMap<AnyNewExpr,Map<SootField,Set<AnyNewExpr>>>(); // Process work-list. while (!worklist.isEmpty()) { // Get the next element AnyNewExpr node = worklist.remove(); // Ignore null pointees from the work-list if (node == null) throw new NullPointerException(); // If this is already there in the new map, then ignore (duplicate // processing) if (newHeap.containsKey(node)) continue; // Otherwise, just get the original stuff back // No need to do deep copy as (1) it is our own data and (2) target // nodes of edges will be reachable // (3) Also oldHeap.get(node) should return an unmodifiable map newHeap.put(node, oldHeap.get(node)); // Add targets of this node to the work-list. for (Set<AnyNewExpr> targets : newHeap.get(node).values()) { worklist.addAll(targets); } } // Set this heap to the new minimal heap this.heap = newHeap; } /** * Loads a field of an object into a root variable. */ public void getField(Local lhs, Local rhs, SootField field) { // Find whatever the RHS->F was pointing to. Set<AnyNewExpr> rhsPointees = roots.get(rhs); Set<AnyNewExpr> rhsFieldPointees = new HashSet<AnyNewExpr>(); for (AnyNewExpr src : rhsPointees) { if (src == null) { throw new NullPointerException(); } else if (src == SUMMARY_NODE) { rhsFieldPointees.add(SUMMARY_NODE); }else if (heap.get(src).containsKey(field)) { Set<AnyNewExpr> targets = heap.get(src).get(field); rhsFieldPointees.addAll(targets); } } // Add the indirect pointees to the LHS edges roots.put(lhs, Collections.unmodifiableSet(rhsFieldPointees)); // Ensure reachability. gc(); } /** * Returns the points-to set of a root variable. */ public Set<AnyNewExpr> getTargets(Local local) { return roots.get(local); } /** * {@inheritDoc} */ @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((heap == null) ? 0 : heap.hashCode()); result = prime * result + ((roots == null) ? 0 : roots.hashCode()); return result; } /** * Removes all out-edges of the given variable */ public void kill(Local v) { // Kill the edges roots.remove(v); // Ensure reachability. gc(); } /** * Creates a new node (if it doesn't already exist) in the heap. * */ private void newNode(AnyNewExpr allocSite, boolean summarizeFields) { // Do not re-create a new node unless we are summarizing its fields if (heap.containsKey(allocSite) && !summarizeFields) { return; } // OK, we have to create it. Lets find it's fields. List<SootField> fields = new LinkedList<SootField>(); // If we are going to summarise fields later, ensure summary node exists if (summarizeFields) { newNode(SUMMARY_NODE, false); } // First decide properly which type of new expression we have if (allocSite instanceof NewExpr) { // Enumerate fields only for non-summary nodes if (allocSite != SUMMARY_NODE) { // Find all reference-like fields from the soot class of the new expression SootClass sootClass = ((RefType)((NewExpr) allocSite).getType()).getSootClass(); while(true) { for (SootField field : sootClass.getFields()) { if (field.isStatic() == false && field.getType() instanceof RefLikeType) { fields.add(field); } } // Get fields for all classes up to java.lang.Object if (sootClass.hasSuperclass()) { sootClass = sootClass.getSuperclass(); } else { break; } } } } else if (allocSite instanceof NewArrayExpr) { // Has only one field: the element[] fields.add(ARRAY_FIELD); } else if (allocSite instanceof NewMultiArrayExpr) { // A multi-dimensional array creation // We do not handle multi-arrays right now allocSite = SUMMARY_NODE; } // Create edges Map<SootField,Set<AnyNewExpr>> edges = new HashMap<SootField,Set<AnyNewExpr>>(); for (SootField field : fields) { HashSet<AnyNewExpr> targets = new HashSet<AnyNewExpr>(); if (summarizeFields) { targets.add(SUMMARY_NODE); } edges.put(field, Collections.unmodifiableSet(targets)); } heap.put(allocSite, Collections.unmodifiableMap(edges)); } /** * Stores values pointed-to by one root variable into a field of objects pointed-to by another root variable. */ public void setField(Local lhs, SootField field, Local rhs) { // You can't set field of a non-existent variable. assert_tmp (roots.containsKey(lhs)); // Since we are doing weak updates, nothing to do if setting field to null if (rhs == null) return; // Find the objects whose field is being modified. Set<AnyNewExpr> lhsPointees = roots.get(lhs); // Find the objects to which the fields will now point to. Set<AnyNewExpr> rhsPointees = roots.get(rhs); // LHS variable should exist if (lhsPointees == null) throw new NullPointerException(); // RHS variable should exist if (rhsPointees == null) throw new NullPointerException(); // If the RHS variable exists, but points to nothing, then bye-bye if(rhsPointees.size() == 0) return; boolean summarizeRhsTargets = false; // For each object that the LHS points to, add to it's field the RHS pointee for (AnyNewExpr node : lhsPointees) { if (node == null) throw new NullPointerException(); if (node == SUMMARY_NODE) { // Don't add any edge to SUMMARY, but note it down so // that we can summarize fields of RHS pointees summarizeRhsTargets = true; continue; } // Add the new edges (copy-and-modify as edges are immutable) Map<SootField,Set<AnyNewExpr>> oldEdges = heap.get(node); Map<SootField,Set<AnyNewExpr>> newEdges = new HashMap<SootField,Set<AnyNewExpr>>(oldEdges); Set<AnyNewExpr> oldTargets = oldEdges.get(field); if (oldTargets == null) { if (node == GLOBAL_SITE) { // If the node is global, then field must be static assert_tmp(field.isStatic()); // In that case, we allow this condition oldTargets = new HashSet<AnyNewExpr>(); } else { // Otherwise not acceptable as we are doing type-checking System.err.println(this); throw new RuntimeException("Field not found: " + field + " in " + node); } } Set<AnyNewExpr> newTargets = new HashSet<AnyNewExpr>(oldTargets); boolean change = newTargets.addAll(rhsPointees); if (change) { newEdges.put(field, Collections.unmodifiableSet(newTargets)); heap.put(node, Collections.unmodifiableMap(newEdges)); } } // If the LHS local pointed to SUMMARY, then we must summarize all // fields of the RHS pointees as we do not know if any such edge // could be created (since we are not maintaining edges out of SUMMARY) if (summarizeRhsTargets) { summarizeTargetFields(rhs); } } /** * Stores a constant into a field of objects pointed-to by a root variable. */ public void setFieldConstant(Local lhs, SootField field, Constant rhs) { // Find out the alloc site of the constant NewExpr newExpr = constantNewExpr(rhs); // If null, do nothing, as we handle only weak updates, // otherwise, add the edge if (newExpr != null) { setFieldNew(lhs, field, newExpr); } } /** * Stores a new object into a field of objects pointed-to by a root variable. */ public void setFieldNew(Local lhs, SootField field, AnyNewExpr allocSite) { // You can't set field of a non-existent variable. assert_tmp (roots.containsKey(lhs)); // Create this node in the heap, if it doesn't already exist newNode(allocSite, false); // Find the objects whose field is being modified. Set<AnyNewExpr> lhsPointees = roots.get(lhs); // Find the objects to which the fields will now point to. Set<AnyNewExpr> rhsPointees = new HashSet<AnyNewExpr>(); rhsPointees.add(allocSite); // LHS variable should exist if (lhsPointees == null) throw new NullPointerException(); // For each object that the LHS points to, add to it's field the RHS pointee for (AnyNewExpr node : lhsPointees) { if (node == null) throw new NullPointerException(); if (node == SUMMARY_NODE) // Don't add any edge to SUMMARY continue; // Add the new edges (copy-and-modify as edges are immutable) Map<SootField,Set<AnyNewExpr>> oldEdges = heap.get(node); Map<SootField,Set<AnyNewExpr>> newEdges = new HashMap<SootField,Set<AnyNewExpr>>(oldEdges); Set<AnyNewExpr> oldTargets = oldEdges.get(field); // Check to see if this node had edges with the given field if (oldTargets == null) { if (node == GLOBAL_SITE) { // If the node is global, then field must be static assert_tmp(field.isStatic()); // In that case, we allow this condition oldTargets = new HashSet<AnyNewExpr>(); } else { // Otherwise not acceptable as we are doing type-checking throw new RuntimeException("Field not found: " + field + " in " + node); } } Set<AnyNewExpr> newTargets = new HashSet<AnyNewExpr>(oldTargets); boolean change = newTargets.addAll(rhsPointees); if (change) { newEdges.put(field, Collections.unmodifiableSet(newTargets)); heap.put(node, Collections.unmodifiableMap(newEdges)); } } } /** * Stores the summary node into a field of objects pointed-to by a root variable. */ public void setFieldSummary(Local lhs, SootField field) { setFieldNew(lhs, field, SUMMARY_NODE); } /** * Removes nodes contained in the argument. This is used at * call-edges. */ public void subtractHeap(PointsToGraph other) { for (AnyNewExpr heapNode : other.heap.keySet()) { this.heap.remove(heapNode); } } public void summarizeTargetFields(Local lhs) { Set<AnyNewExpr> targets = roots.get(lhs); // Summarize nodes for (AnyNewExpr allocSite : targets) { newNode(allocSite, true); } } @Override public String toString() { StringBuffer sb = new StringBuffer(); for (Local var : roots.keySet()) { sb.append(var).append(" -> "); for (AnyNewExpr node : roots.get(var)) { sb.append(node2String(node)).append(" "); } sb.append("\n"); } for (AnyNewExpr source : heap.keySet()) { for (SootField field : heap.get(source).keySet()) { sb.append(node2String(source)).append(".").append(field.isStatic() ? field.toString() : field.getName()).append(" -> "); for (AnyNewExpr target : heap.get(source).get(field)) { sb.append(node2String(target)).append(" "); } sb.append("\n"); } } return sb.toString(); } private String node2String(AnyNewExpr node) { if (node == SUMMARY_NODE) { return "SUMMARY"; } else if (node == STRING_SITE) { return "STRING"; } else if (node == CLASS_SITE) { return "CLASS"; } else { return "[" + node.toString() + " (" + node.hashCode() + ")]"; } } /** * Sets this graph to the union of the given arguments. */ public void union(PointsToGraph p, PointsToGraph q) { // Ensure that we are not the operands (otherwise the clear will cause // an issue) assert_tmp (this != p && this != q); // Clear the current data. this.roots = new HashMap<Local,Set<AnyNewExpr>>(); this.heap = new HashMap<AnyNewExpr,Map<SootField,Set<AnyNewExpr>>>(); // Union root variable edges. Set<Local> vars1 = p.roots.keySet(); Set<Local> vars2 = q.roots.keySet(); Set<Local> allVars = new HashSet<Local>(); allVars.addAll(vars1); allVars.addAll(vars2); for (Local v : allVars) { // Collect all pointees Set<AnyNewExpr> pointees = new HashSet<AnyNewExpr>(); if (vars1.contains(v)) { pointees.addAll(p.roots.get(v)); } if (vars2.contains(v)) { pointees.addAll(q.roots.get(v)); } // Add an immutable version of these pointees this.roots.put(v, Collections.unmodifiableSet(pointees)); } // Time to union all heap nodes Set<AnyNewExpr> nodes1 = p.heap.keySet(); Set<AnyNewExpr> nodes2 = q.heap.keySet(); Set<AnyNewExpr> allNodes = new HashSet<AnyNewExpr>(); allNodes.addAll(nodes1); allNodes.addAll(nodes2); for (AnyNewExpr node : allNodes) { Map<SootField,Set<AnyNewExpr>> edges = new HashMap<SootField,Set<AnyNewExpr>>(); // First, find all possible fields Set<SootField> fields = new HashSet<SootField>(); if (p.heap.containsKey(node)) fields.addAll(p.heap.get(node).keySet()); if (q.heap.containsKey(node)) fields.addAll(q.heap.get(node).keySet()); // Now, initialise each field with a mutable set of pointees for (SootField field : fields) { edges.put(field, new HashSet<AnyNewExpr>()); } // Add field edges from first operand if (nodes1.contains(node)) { for (SootField field : fields) { edges.get(field).addAll(p.heap.get(node).get(field)); } } // Add field edges from second operand if (nodes2.contains(node)) { for (SootField field : fields) { edges.get(field).addAll(q.heap.get(node).get(field)); } } // Immutalize the field edges for (SootField field : fields) { edges.put(field, Collections.unmodifiableSet(edges.get(field))); } // Add an immutable version of these edges this.heap.put(node, Collections.unmodifiableMap(edges)); } } private void assert_tmp(boolean b) { if (b == false) throw new AssertionError(); } public void killWithoutGC(Local local) { roots.remove(local); } }