458 lines
19 KiB
Scala
458 lines
19 KiB
Scala
/*
|
|
* (C) Copyright 2010-2015 SAP SE.
|
|
*
|
|
* All rights reserved. This program and the accompanying materials
|
|
* are made available under the terms of the Eclipse Public License v1.0
|
|
* which accompanies this distribution, and is available at
|
|
* http://www.eclipse.org/legal/epl-v10.html
|
|
*
|
|
*/
|
|
|
|
package com.logicalhacking.dasca.crosslanguage.builder
|
|
|
|
import scala.collection.JavaConverters._
|
|
import com.ibm.wala.ipa.callgraph.CGNode
|
|
import com.ibm.wala.ipa.callgraph.CallGraph
|
|
import com.ibm.wala.classLoader.CallSiteReference
|
|
import com.ibm.wala.classLoader.Language
|
|
import scala.xml.XML
|
|
import scala.xml.Elem
|
|
import scala.collection.JavaConverters._
|
|
import com.ibm.wala.classLoader.CallSiteReference
|
|
import com.ibm.wala.ipa.callgraph.CGNode
|
|
import com.ibm.wala.cast.js.loader.JavaScriptLoader
|
|
import com.ibm.wala.ssa.SSAAbstractInvokeInstruction
|
|
import com.ibm.wala.ssa.SymbolTable
|
|
import scala.xml.Elem
|
|
import scala.xml.Node
|
|
import com.typesafe.scalalogging.Logger
|
|
import org.slf4j.LoggerFactory
|
|
import com.ibm.wala.cast.js.loader.JavaScriptLoader
|
|
import com.ibm.wala.ssa.SSAInstruction
|
|
import com.ibm.wala.cast.js.ssa.JavaScriptInvoke
|
|
import com.ibm.wala.cast.js.types.JavaScriptMethods
|
|
import com.ibm.wala.ssa.SSAReturnInstruction
|
|
import com.ibm.wala.ssa.SSAPhiInstruction
|
|
import com.ibm.wala.cast.ir.ssa.AstLexicalRead
|
|
import com.ibm.wala.cast.ir.ssa.AstLexicalWrite
|
|
import scala.{ Option => ? }
|
|
import com.ibm.wala.cast.ir.ssa.AstLexicalAccess.Access
|
|
import scala.collection.immutable.HashMap
|
|
import com.ibm.wala.ssa.SSAGetInstruction
|
|
import com.logicalhacking.dasca.crosslanguage.builder.algorithms.ExecuteActionBasedChecker
|
|
import com.ibm.wala.ipa.cfg.BasicBlockInContext
|
|
import com.ibm.wala.ssa.analysis.IExplodedBasicBlock
|
|
import com.logicalhacking.dasca.crosslanguage.builder.algorithms.ExecuteActionBasedChecker
|
|
import com.ibm.wala.ssa.analysis.ExplodedControlFlowGraph
|
|
import com.logicalhacking.dasca.crosslanguage.util.Util
|
|
import scala.collection.mutable.Queue
|
|
import scala.collection.mutable.ListBuffer
|
|
import com.ibm.wala.cast.ir.ssa.AstIRFactory
|
|
import scala.collection.mutable.LinkedHashSet
|
|
import com.ibm.wala.classLoader.IMethod
|
|
import com.ibm.wala.cast.ir.ssa.AstIRFactory.AstIR
|
|
import com.ibm.wala.cast.ir.ssa.AstIRFactory.AstIR
|
|
|
|
class MergedCallGraph(val javaCG: CallGraph, val jsCG: CallGraph, val configXml: Elem) extends Iterable[CGNode] {
|
|
val logger = Logger(LoggerFactory.getLogger(getClass.toString))
|
|
var targets = Map[(CGNode, CallSiteReference), LinkedHashSet[CGNode]]()
|
|
var origins = Map[CGNode, LinkedHashSet[(CGNode, CallSiteReference)]]()
|
|
var paramInfo = Map[(CGNode, CallSiteReference), String]()
|
|
var paramMap = Map[(CGNode, CallSiteReference, CGNode), Map[Int, Set[Int]]]()
|
|
|
|
|
|
var filterJavaCallSites = false
|
|
var filterJSFrameworks = false
|
|
|
|
lazy val classNames = {
|
|
for (
|
|
feature <- configXml \\ "feature";
|
|
param <- feature \ "param" if param \@ "name" == "android-package"
|
|
) yield (feature \@ "name", param \@ "value")
|
|
}.groupBy(_._1).mapValues(_.map("L" + _._2.replace('.', '/')))
|
|
|
|
lazy val lexicalWrites = {
|
|
for (
|
|
node <- jsCG.iterator().asScala;
|
|
write <- node.getIR().getInstructions.collect { case inst: AstLexicalWrite => inst };
|
|
access <- write.getAccesses();
|
|
use <- 0.to(write.getNumberOfUses()).find(use => write.getUse(use) == access.valueNumber)
|
|
) yield ((access.variableDefiner, access.variableName), (node, write, use))
|
|
}.toSeq.groupBy(_._1).mapValues(_.map(_._2))
|
|
|
|
lazy val executeNodesForClassName = {
|
|
for (
|
|
node <- javaCG.iterator.asScala;
|
|
method = node.getMethod if method.getName.toString() == "execute";
|
|
declaringClass = method.getDeclaringClass if Util.derivesFromCordovaPlugin(declaringClass.getSuperclass)
|
|
) yield (declaringClass.getName.toString(), node)
|
|
}.toSeq.groupBy(_._1).mapValues(_.map(_._2))
|
|
|
|
lazy val (successCalls, failCalls) = {
|
|
val successLb = ListBuffer[(CGNode, SSAAbstractInvokeInstruction)]()
|
|
val failLb = ListBuffer[(CGNode, SSAAbstractInvokeInstruction)]()
|
|
|
|
for (
|
|
node <- javaCG.iterator().asScala;
|
|
ir <- ?(node.getIR());
|
|
invoke <- ir.getInstructions().collect({ case invoke: SSAAbstractInvokeInstruction => invoke });
|
|
csr = invoke.getCallSite()
|
|
) {
|
|
if (isSuccessCall(node, csr)) successLb += ((node, invoke))
|
|
if (isFailCall(node, csr)) failLb += ((node, invoke))
|
|
}
|
|
|
|
(successLb.toList, failLb.toList)
|
|
}
|
|
|
|
override def iterator(): Iterator[CGNode] = {
|
|
javaCG.iterator.asScala ++ jsCG.iterator.asScala
|
|
}
|
|
|
|
def getPossibleTargets(node: CGNode, csr: CallSiteReference) = {
|
|
if (Util.isJavaNode(node)) {
|
|
javaCG.getPossibleTargets(node, csr).asScala
|
|
} else {
|
|
jsCG.getPossibleTargets(node, csr).asScala
|
|
}
|
|
}
|
|
|
|
def connect(filterJavaCallSites: Boolean, filterJSFrameworks: Boolean): Unit = {
|
|
this.filterJavaCallSites = filterJavaCallSites
|
|
this.filterJSFrameworks = filterJSFrameworks
|
|
connect()
|
|
}
|
|
|
|
def connect(): Unit = for (
|
|
node <- jsCG.iterator.asScala;
|
|
ir = node.getIR if ir != null;
|
|
csr <- ir.iterateCallSites.asScala;
|
|
jsExecuteNode <- findJsExecuteNode(node, csr);
|
|
invoke <- ir.getCalls(csr);
|
|
st = ir.getSymbolTable;
|
|
featureName <- findFeatureString(st, node, invoke);
|
|
classNames <- classNames.get(featureName);
|
|
className <- classNames;
|
|
executeNodes <- executeNodesForClassName.get(className);
|
|
executeNode <- executeNodes;
|
|
action <- determinePossibleActions(node, invoke)
|
|
) {
|
|
val paramMap = Map(2 -> Set(3), 3 -> Set(3), 6 -> Set(2))
|
|
addCrossCall(node, csr, executeNode, paramMap, action)
|
|
|
|
val preds = jsCG.getPredNodes(node).asScala.toSeq
|
|
logger.debug(s"There are ${preds.size} predecessors of node ${Util.prettyPrintInstruction(node, node.getIR.getInstructions.find { _ != null } get)}")
|
|
for (
|
|
pred <- preds;
|
|
site <- jsCG.getPossibleSites(pred, node).asScala;
|
|
invoke <- pred.getIR.getCalls(site)
|
|
) logger.debug("\t" + Util.prettyPrintInstruction(pred, invoke))
|
|
|
|
def getPackagePrefix(pkg: String) = {
|
|
val firstIndex = pkg.indexOf('/')
|
|
val secondIndex = pkg.indexOf('/', firstIndex + 1)
|
|
if (secondIndex == -1) {
|
|
pkg
|
|
} else {
|
|
pkg.substring(0, secondIndex)
|
|
}
|
|
}
|
|
|
|
val packageFilter = { n: CGNode =>
|
|
getPackagePrefix(n.getMethod.getDeclaringClass.getName.getPackage.toString()) ==
|
|
getPackagePrefix(executeNode.getMethod.getDeclaringClass.getName.getPackage.toString()) &&
|
|
!("org/apache/cordova".equals(n.getMethod.getDeclaringClass.getName.getPackage.toString()))
|
|
}
|
|
|
|
val reachabilityChecker = new ExecuteActionBasedChecker(javaCG, packageFilter, action, executeNode)
|
|
|
|
val successNodes = findFunctionNodes(2, node, invoke, jsExecuteNode)
|
|
logger.info(s"Found ${successNodes.size} success nodes for action $action, ${Util.prettyPrintInstruction(node, invoke)}");
|
|
val reachableSuccessCalls = if (filterJavaCallSites) {
|
|
successCalls.filter({ case (successNode, successInvoke) => reachabilityChecker.isReachable(executeNode, successNode, successInvoke) })
|
|
} else {
|
|
successCalls.filter({ case (n, _) => packageFilter(n) })
|
|
}
|
|
for (
|
|
successTarget <- successNodes;
|
|
_ = logger.debug(s"\t-> ${Util.prettyPrintInstruction(successTarget, successTarget.getIR.getInstructions.find { _ != null } get)}");
|
|
(successFrom, successInvoke) <- reachableSuccessCalls
|
|
) {
|
|
addCrossCall(successFrom, successInvoke.getCallSite, successTarget, Map(1 -> Set(3)))
|
|
}
|
|
|
|
val failNodes = findFunctionNodes(3, node, invoke, jsExecuteNode)
|
|
logger.info(s"Found ${failNodes.size} fail nodes for action $action, ${Util.prettyPrintInstruction(node, invoke)}")
|
|
val reachableFailCalls = if (filterJavaCallSites) {
|
|
failCalls.filter({ case (failNode, failInvoke) => reachabilityChecker.isReachable(executeNode, failNode, failInvoke) })
|
|
} else {
|
|
failCalls.filter({ case (n, _) => packageFilter(n) })
|
|
}
|
|
for (
|
|
failTarget <- failNodes;
|
|
_ = logger.debug(s"\t-> ${Util.prettyPrintInstruction(failTarget, failTarget.getIR.getInstructions.find { _ != null } get)}");
|
|
(failFrom, failInvoke) <- reachableFailCalls
|
|
) {
|
|
addCrossCall(failFrom, failInvoke.getCallSite, failTarget, Map(1 -> Set(3)))
|
|
}
|
|
}
|
|
|
|
def findFeatureString(st: SymbolTable, node: CGNode, invoke: SSAAbstractInvokeInstruction) = {
|
|
val featureUse = invoke.getUse(4)
|
|
if (st.isStringConstant(featureUse)) {
|
|
Option(st.getStringValue(featureUse))
|
|
} else {
|
|
logger.warn(s"Feature parameter was not found in symbol table for statement ${Util.prettyPrintInstruction(node, invoke)}")
|
|
None
|
|
}
|
|
}
|
|
|
|
def determinePossibleActions(node: CGNode, invoke: SSAAbstractInvokeInstruction) = {
|
|
val lb = ListBuffer[String]()
|
|
val st = node.getIR.getSymbolTable
|
|
if (st.isStringConstant(invoke.getUse(5))) {
|
|
lb += st.getStringValue(invoke.getUse(5));
|
|
} else {
|
|
node.getDU.getDef(invoke.getUse(5)) match {
|
|
case phi: SSAPhiInstruction => {
|
|
if (0.until(phi.getNumberOfUses).map({ i => st.isStringConstant(phi.getUse(i)) }).forall { _ == true }) {
|
|
lb ++= 0.until(phi.getNumberOfUses).map { i => st.getStringValue(phi.getUse(i)) }
|
|
}
|
|
}
|
|
case _ =>
|
|
}
|
|
}
|
|
lb.toList
|
|
}
|
|
|
|
def isSuccessCall(node: CGNode, csr: CallSiteReference): Boolean = {
|
|
val declaredTarget = csr.getDeclaredTarget
|
|
if (declaredTarget.getDeclaringClass().getName().toString() != "Lorg/apache/cordova/CallbackContext")
|
|
return false
|
|
|
|
if (declaredTarget.getName().toString() == "success")
|
|
return true
|
|
|
|
if (declaredTarget.getName().toString() == "sendPluginResult" && isPossibleStatus(node, csr, "OK"))
|
|
return true
|
|
|
|
return false
|
|
}
|
|
|
|
def findJsExecuteNode(node: CGNode, csr: CallSiteReference) = getPossibleTargets(node, csr).collectFirst({
|
|
n =>
|
|
n.getMethod match {
|
|
case m: JavaScriptLoader#DynamicMethodObject if (m.getEntity.getName.endsWith(CordovaCGBuilder.ExecuteSuffix)) => n
|
|
}
|
|
})
|
|
|
|
private def addCrossCall(from: CGNode, csr: CallSiteReference, to: CGNode, params: Map[Int, Set[Int]]): Unit = {
|
|
if (filterJSFrameworks && skipCrossCall(from, csr, to)) {
|
|
logger.info(s"Skipped cross call from ${Util.prettyPrintInstruction(from, from.getIR.getCalls(csr)(0))} to ${Util.prettyPrintInstruction(to, to.getIR.getInstructions.find({ _ != null }).get)}")
|
|
return
|
|
}
|
|
|
|
targets += (((from, csr), LinkedHashSet(to) ++ targets.getOrElse((from, csr), Set())))
|
|
origins += ((to, LinkedHashSet((from, csr)) ++ origins.getOrElse(to, Set())))
|
|
paramMap += (((from, csr, to), params))
|
|
}
|
|
|
|
private def skipCrossCall(from: CGNode, csr: CallSiteReference, to: CGNode): Boolean = {
|
|
if (Util.isJavaNode(from)) {
|
|
to.getMethod match {
|
|
case m: JavaScriptLoader#DynamicMethodObject if (m.getEntity.getName.startsWith("make_")) => return true
|
|
case _ =>
|
|
}
|
|
to.getIR match {
|
|
case ir: AstIR => {
|
|
val (_, _, _, _, relPath:String) = Util.getJavaScriptSourceInfo(ir, ir.getInstructions.find(_ != null).get)
|
|
val lc = relPath.toLowerCase()
|
|
val filename = lc.substring(lc.lastIndexOf('/'))
|
|
if (filename.contains("jquery") || filename.contains("energize") || filename.contains("jqm") || filename.contains("prologue.js") ||
|
|
filename.contains("preamble.js") || filename.contains("autonumeric") || filename.contains("modernizr") || filename.contains("cordova")) {
|
|
true
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
case _ => true
|
|
}
|
|
} else {
|
|
from.getIR match {
|
|
case ir: AstIR => {
|
|
val (_, _, _, _, relPath:String) = Util.getJavaScriptSourceInfo(ir, ir.getInstructions.find(_ != null).get)
|
|
val lc = relPath.toLowerCase()
|
|
val filename = lc.substring(lc.lastIndexOf('/'))
|
|
if (filename.contains("cordova")) {
|
|
true
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
case _ => true
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return all possible targets nodes (i.e., representing methods that are possibly invoked (either
|
|
* within the same programming language or cross language) by the implementation of the method
|
|
* that the node {@code node} represents).
|
|
* @param cg bvn
|
|
* @param node
|
|
* @return
|
|
*/
|
|
def getAllPossibleTargetNodes(node: CGNode) = {
|
|
val ir = node.getIR();
|
|
var targetNodes = Set[CGNode]();
|
|
|
|
if (Util.isJavaNode(node)) {
|
|
// Java Node
|
|
val it = node.iterateCallSites();
|
|
while (it.hasNext()) {
|
|
targetNodes= targetNodes.union(this.getPossibleTargets(node, it.next()));
|
|
}
|
|
} else {
|
|
// JavaScript Node
|
|
val it = ir.iterateAllInstructions();
|
|
while (it.hasNext()) {
|
|
val ssa = it.next();
|
|
if (ssa.isInstanceOf[JavaScriptInvoke]) {
|
|
val invoke = ssa.asInstanceOf[JavaScriptInvoke];
|
|
// see http://wala.sourceforge.net/files/WALAJavaScriptTutorial.pdf, page 11
|
|
if (invoke.getCallSite().getDeclaredTarget().equals(
|
|
JavaScriptMethods.dispatchReference)) {
|
|
targetNodes = targetNodes.union(this.getPossibleTargets(node, invoke.getCallSite()));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// get Cross Calls
|
|
val it = node.iterateCallSites();
|
|
while (it.hasNext()) {
|
|
targetNodes = targetNodes.union(this.getCrossTargets(node, it.next()));
|
|
}
|
|
targetNodes;
|
|
}
|
|
|
|
private def addCrossCall(from: CGNode, csr: CallSiteReference, to: CGNode, params: Map[Int, Set[Int]], param: String): Unit = {
|
|
addCrossCall(from, csr, to, params)
|
|
paramInfo += (((from, csr), param))
|
|
}
|
|
|
|
def getAllCrossTargets = targets
|
|
|
|
def getCrossOrigins(node: CGNode) = origins.get(node) match {
|
|
case Some(x) => x
|
|
case None => LinkedHashSet[(CGNode, CallSiteReference)]()
|
|
}
|
|
|
|
def getCrossTargets(node: CGNode, csr: CallSiteReference): LinkedHashSet[CGNode] = targets.get((node, csr)) match {
|
|
case Some(x) => x
|
|
case None => LinkedHashSet[CGNode]()
|
|
}
|
|
|
|
|
|
|
|
def getParameterMapping(from: CGNode, csr: CallSiteReference, to: CGNode) = paramMap.get((from, csr, to))
|
|
|
|
def findFunctionNodes(arg: Int, node: CGNode, inst: SSAInstruction, jsExecuteNode: CGNode) = {
|
|
for (
|
|
invoke <- jsExecuteNode.getDU.getUses(arg + 1).asScala.collectFirst({ case s: SSAAbstractInvokeInstruction => s });
|
|
csr = invoke.getCallSite
|
|
) yield jsCG.getPossibleTargets(jsExecuteNode, csr).asScala
|
|
}.getOrElse(Set[CGNode]())
|
|
// val queue = Queue((arg, node, inst))
|
|
// val visited = scala.collection.mutable.Set[(Int, CGNode, SSAInstruction)]()
|
|
// val result = ListBuffer[CGNode]()
|
|
// while (!queue.isEmpty) {
|
|
// val cur = queue.dequeue()
|
|
// if (!visited.contains(cur)) {
|
|
// visited += cur
|
|
// val (curArg, curNode, curInst) = cur
|
|
//
|
|
// val use = curInst.getUse(curArg)
|
|
// val symbolTable = curNode.getIR().getSymbolTable()
|
|
//
|
|
// if (use <= curNode.getMethod().getNumberOfParameters()) {
|
|
// for (
|
|
// pred <- jsCG.getPredNodes(curNode).asScala;
|
|
// csr <- jsCG.getPossibleSites(pred, curNode).asScala;
|
|
// invoke <- pred.getIR().getCalls(csr) if invoke.getNumberOfParameters() == curNode.getMethod().getNumberOfParameters()
|
|
// ) queue += ((use - 1, pred, invoke))
|
|
// } else {
|
|
// curNode.getDU().getDef(use) match {
|
|
// case invoke: JavaScriptInvoke if isMethodConstructor(invoke, symbolTable) => result ++= findCallbackNode(jsExecuteNode, invoke, symbolTable).toList
|
|
// case invoke: JavaScriptInvoke => for (
|
|
// target <- jsCG.getPossibleTargets(curNode, invoke.getCallSite()).asScala;
|
|
// ret <- target.getIR().getInstructions().collect({ case r: SSAReturnInstruction => r });
|
|
// if (ret.getNumberOfUses == 1)
|
|
// ) queue += ((0, target, ret))
|
|
// case phi: SSAPhiInstruction => for (i <- 0.until(phi.getNumberOfUses)) queue += ((i, curNode, phi))
|
|
// case read: AstLexicalRead => {
|
|
// for (
|
|
// node <- jsCG.iterator().asScala;
|
|
// method <- ?(node.getMethod()).collect({ case method: JavaScriptLoader#JavaScriptMethodObject => method }).toSeq;
|
|
// access <- read.getAccesses() if access.variableName == method.getEntity().getName();
|
|
// pkg <- ?(method.getReference().getDeclaringClass().getName().getPackage()) if pkg.toString() == access.variableDefiner.substring(1)
|
|
// ) yield result += node
|
|
// for (
|
|
// accessNo <- 0.until(read.getAccessCount);
|
|
// access = read.getAccesses().apply(accessNo);
|
|
// writes <- lexicalWrites.get((access.variableDefiner, access.variableName));
|
|
// write <- writes
|
|
// ) queue += ((accessNo, write._1, write._2))
|
|
// }
|
|
// case _ =>
|
|
// }
|
|
// }
|
|
// }
|
|
// }
|
|
// result.toList
|
|
|
|
def findCallbackNode(jsExecNode: CGNode, inst: SSAInstruction, symbolTable: SymbolTable) = jsCG.getSuccNodes(jsExecNode).asScala.find {
|
|
_.getMethod.toString().contains(symbolTable.getStringValue(inst.getUse(1)))
|
|
}
|
|
|
|
def isMethodConstructor(invoke: JavaScriptInvoke, symbolTable: SymbolTable) =
|
|
invoke.getCallSite().getDeclaredTarget() == JavaScriptMethods.ctorReference &&
|
|
invoke.getNumberOfUses > 1 && symbolTable.isStringConstant(invoke.getUse(1))
|
|
|
|
def isPossibleStatus(node: CGNode, csr: CallSiteReference, args: String*): Boolean = {
|
|
for (
|
|
invoke <- node.getIR().getCalls(csr);
|
|
inv <- node.getDU().getUses(invoke.getUse(1)).asScala.collect({ case i: SSAAbstractInvokeInstruction => i }) if inv != invoke;
|
|
if inv.getCallSite().getDeclaredTarget().getName().toString() == "<init>";
|
|
statusUse = inv.getUse(1)
|
|
) {
|
|
if (statusUse <= node.getMethod().getNumberOfParameters())
|
|
return true
|
|
for (
|
|
get <- ?(node.getDU().getDef(statusUse)).collect({ case i: SSAGetInstruction => i })
|
|
) {
|
|
if (args.contains(get.getDeclaredField().getName().toString()))
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
def isFailCall(node: CGNode, csr: CallSiteReference): Boolean = {
|
|
val declaredTarget = csr.getDeclaredTarget
|
|
if (declaredTarget.getDeclaringClass().getName().toString() != "Lorg/apache/cordova/CallbackContext")
|
|
return false
|
|
if (declaredTarget.getName().toString() == "error")
|
|
return true
|
|
|
|
if (declaredTarget.getName().toString() == "sendPluginResult" && isPossibleStatus(node, csr, "INVALID_ACTION", "ERROR", "JSON_EXCEPTION", "IO_EXCEPTION"))
|
|
return true
|
|
|
|
return false
|
|
}
|
|
|
|
def getNumberOfNodes = javaCG.getNumberOfNodes + jsCG.getNumberOfNodes
|
|
|
|
|
|
|
|
}
|