Java: Add support for callback-based library models.

This commit is contained in:
Anders Schack-Mulligen
2021-09-13 14:41:37 +02:00
parent d0563c80be
commit 89a6cdc711
10 changed files with 245 additions and 35 deletions

View File

@@ -7,10 +7,10 @@ private import semmle.code.java.dispatch.VirtualDispatch as VirtualDispatch
private module DispatchImpl {
/** Gets a viable implementation of the target of the given `Call`. */
Callable viableCallable(Call c) {
result = VirtualDispatch::viableCallable(c)
DataFlowCallable viableCallable(DataFlowCall c) {
result = VirtualDispatch::viableCallable(c.asCall())
or
result.(SummarizedCallable) = c.getCallee().getSourceDeclaration()
result.(SummarizedCallable) = c.asCall().getCallee().getSourceDeclaration()
}
/**
@@ -45,7 +45,7 @@ private module DispatchImpl {
private predicate relevantContext(Call ctx, int i) {
exists(Callable c |
mayBenefitFromCallContext(_, c, i) and
c = viableCallable(ctx)
c = VirtualDispatch::viableCallable(ctx)
)
}
@@ -88,24 +88,25 @@ private module DispatchImpl {
}
/**
* Holds if the set of viable implementations that can be called by `ma`
* Holds if the set of viable implementations that can be called by `call`
* might be improved by knowing the call context. This is the case if the
* qualifier is a parameter of the enclosing callable `c`.
*/
predicate mayBenefitFromCallContext(MethodAccess ma, Callable c) {
mayBenefitFromCallContext(ma, c, _)
predicate mayBenefitFromCallContext(DataFlowCall call, DataFlowCallable c) {
mayBenefitFromCallContext(call.asCall(), c, _)
}
/**
* Gets a viable dispatch target of `ma` in the context `ctx`. This is
* restricted to those `ma`s for which a context might make a difference.
* Gets a viable dispatch target of `call` in the context `ctx`. This is
* restricted to those `call`s for which a context might make a difference.
*/
Method viableImplInCallContext(MethodAccess ma, Call ctx) {
result = viableCallable(ma) and
exists(int i, Callable c, Method def, RefType t, boolean exact |
Method viableImplInCallContext(DataFlowCall call, DataFlowCall ctx) {
result = viableCallable(call) and
exists(int i, Callable c, Method def, RefType t, boolean exact, MethodAccess ma |
ma = call.asCall() and
mayBenefitFromCallContext(ma, c, i) and
c = viableCallable(ctx) and
contextArgHasType(ctx, i, t, exact) and
contextArgHasType(ctx.asCall(), i, t, exact) and
ma.getMethod().getSourceDeclaration() = def
|
exact = true and result = VirtualDispatch::exactMethodImpl(def, t.getSourceDeclaration())

View File

@@ -326,12 +326,14 @@ module Private {
* The instance argument is considered to have index `-1`.
*/
predicate argumentOf(DataFlowCall call, int pos) {
exists(Argument arg | this.asExpr() = arg | call = arg.getCall() and pos = arg.getPosition())
exists(Argument arg | this.asExpr() = arg |
call.asCall() = arg.getCall() and pos = arg.getPosition()
)
or
call = this.(ImplicitVarargsArray).getCall() and
pos = call.getCallee().getNumberOfParameters() - 1
call.asCall() = this.(ImplicitVarargsArray).getCall() and
pos = call.asCall().getCallee().getNumberOfParameters() - 1
or
pos = -1 and this = getInstanceArgument(call)
pos = -1 and this = getInstanceArgument(call.asCall())
or
this.(SummaryNode).isArgumentOf(call, pos)
}
@@ -361,7 +363,7 @@ module Private {
/** Gets the underlying call. */
DataFlowCall getCall() {
result = this.asExpr()
result.asCall() = this.asExpr()
or
this.(SummaryNode).isOut(result)
}

View File

@@ -5,6 +5,7 @@ private import DataFlowDispatch
private import semmle.code.java.controlflow.Guards
private import semmle.code.java.dataflow.SSA
private import ContainerFlow
private import semmle.code.java.dataflow.FlowSummary
private import FlowSummaryImpl as FlowSummaryImpl
import DataFlowNodes::Private
@@ -24,7 +25,7 @@ class ReturnKind extends TReturnKind {
* `kind`.
*/
OutNode getAnOutNode(DataFlowCall call, ReturnKind kind) {
result = call.getNode() and
result.getCall() = call and
kind = TNormalReturnKind()
}
@@ -214,9 +215,55 @@ class DataFlowExpr = Expr;
class DataFlowType = RefType;
class DataFlowCall extends Call {
/** Gets the data flow node corresponding to this call. */
ExprNode getNode() { result.getExpr() = this }
private newtype TDataFlowCall =
TCall(Call c) or
TSummaryCall(SummarizedCallable c, Node receiver) {
FlowSummaryImpl::Private::summaryCallbackRange(c, receiver)
}
/** A call relevant for data flow. Includes both source calls and synthesized calls. */
class DataFlowCall extends TDataFlowCall {
/** Gets the source (non-synthesized) call this corresponds to, if any. */
Call asCall() { this = TCall(result) }
/** Gets the enclosing callable of this call. */
abstract DataFlowCallable getEnclosingCallable();
/** Gets a textual representation of this call. */
abstract string toString();
/** Gets the location of this call. */
abstract Location getLocation();
}
/** A source call, that is, a `Call`. */
class SrcCall extends DataFlowCall, TCall {
Call call;
SrcCall() { this = TCall(call) }
override DataFlowCallable getEnclosingCallable() { result = call.getEnclosingCallable() }
override string toString() { result = call.toString() }
override Location getLocation() { result = call.getLocation() }
}
/** A synthesized call inside a `SummarizedCallable`. */
class SummaryCall extends DataFlowCall, TSummaryCall {
private SummarizedCallable c;
private Node receiver;
SummaryCall() { this = TSummaryCall(c, receiver) }
/** Gets the data flow node that this call targets. */
Node getReceiver() { result = receiver }
override DataFlowCallable getEnclosingCallable() { result = c }
override string toString() { result = "[summary] call to " + receiver + " in " + c }
override Location getLocation() { result = c.getLocation() }
}
/** Holds if `e` is an expression that always has the same Boolean value `val`. */
@@ -275,13 +322,23 @@ predicate isImmutableOrUnobservable(Node n) {
/** Holds if `n` should be hidden from path explanations. */
predicate nodeIsHidden(Node n) { n instanceof SummaryNode }
class LambdaCallKind = Unit;
class LambdaCallKind = Method; // the "apply" method in the functional interface
/** Holds if `creation` is an expression that creates a lambda of kind `kind` for `c`. */
predicate lambdaCreation(Node creation, LambdaCallKind kind, DataFlowCallable c) { none() }
predicate lambdaCreation(Node creation, LambdaCallKind kind, DataFlowCallable c) {
exists(FunctionalExpr func, FunctionalInterface interface |
creation.asExpr() = func and
func.asMethod() = c and
func.getType().(RefType).getSourceDeclaration() = interface and
kind = interface.getRunMethod()
)
}
/** Holds if `call` is a lambda call of kind `kind` where `receiver` is the lambda expression. */
predicate lambdaCall(DataFlowCall call, LambdaCallKind kind, Node receiver) { none() }
predicate lambdaCall(DataFlowCall call, LambdaCallKind kind, Node receiver) {
receiver = call.(SummaryCall).getReceiver() and
getNodeDataFlowType(receiver).getSourceDeclaration().(FunctionalInterface).getRunMethod() = kind
}
/** Extra data-flow steps needed for lambda flow analysis. */
predicate additionalLambdaFlowStep(Node nodeFrom, Node nodeTo, boolean preservesValue) { none() }

View File

@@ -151,7 +151,7 @@ predicate simpleLocalFlowStep(Node node1, Node node2) {
ma.getCallee().getSourceDeclaration() = m and m.returnsValue(argNo)
|
node2.asExpr() = ma and
node1.(ArgumentNode).argumentOf(ma, argNo)
node1.(ArgumentNode).argumentOf(any(DataFlowCall c | c.asCall() = ma), argNo)
)
or
FlowSummaryImpl::Private::Steps::summaryLocalStep(node1, node2, true)

View File

@@ -20,7 +20,7 @@ predicate parameterPosition(int i) { i in [-1 .. any(Parameter p).getPosition()]
Node summaryNode(SummarizedCallable c, SummaryNodeState state) { result = getSummaryNode(c, state) }
/** Gets the synthesized data-flow call for `receiver`. */
DataFlowCall summaryDataFlowCall(Node receiver) { none() }
DataFlowCall summaryDataFlowCall(Node receiver) { result.(SummaryCall).getReceiver() = receiver }
/** Gets the type of content `c`. */
DataFlowType getContentType(Content c) { result = c.getType() }
@@ -35,13 +35,18 @@ DataFlowType getReturnType(SummarizedCallable c, ReturnKind rk) {
* Gets the type of the `i`th parameter in a synthesized call that targets a
* callback of type `t`.
*/
DataFlowType getCallbackParameterType(DataFlowType t, int i) { none() }
DataFlowType getCallbackParameterType(DataFlowType t, int i) {
result = getErasedRepr(t.(FunctionalInterface).getRunMethod().getParameterType(i))
}
/**
* Gets the return type of kind `rk` in a synthesized call that targets a
* callback of type `t`.
*/
DataFlowType getCallbackReturnType(DataFlowType t, ReturnKind rk) { none() }
DataFlowType getCallbackReturnType(DataFlowType t, ReturnKind rk) {
result = getErasedRepr(t.(FunctionalInterface).getRunMethod().getReturnType()) and
exists(rk)
}
/**
* Holds if an external flow summary exists for `c` with input specification
@@ -106,13 +111,13 @@ class InterpretNode extends TInterpretNode {
Node asNode() { this = TNode(result) }
/** Gets the call that this node corresponds to, if any. */
DataFlowCall asCall() { result = this.asElement() }
DataFlowCall asCall() { result.asCall() = this.asElement() }
/** Gets the callable that this node corresponds to, if any. */
DataFlowCallable asCallable() { result = this.asElement() }
/** Gets the target of this call, if any. */
Callable getCallTarget() { result = this.asCall().getCallee().getSourceDeclaration() }
Callable getCallTarget() { result = this.asCall().asCall().getCallee().getSourceDeclaration() }
/** Gets a textual representation of this node. */
string toString() {

View File

@@ -142,8 +142,8 @@ private predicate viableParamCand(Call call, int i, ParameterNode p) {
* Holds if `arg` is a possible argument to `p` taking virtual dispatch into account.
*/
private predicate viableArgParamCand(ArgumentNode arg, ParameterNode p) {
exists(int i, Call call |
viableParamCand(call, i, p) and
exists(int i, DataFlowCall call |
viableParamCand(call.asCall(), i, p) and
arg.argumentOf(call, i)
)
}

View File

@@ -43,8 +43,8 @@ private predicate viableParam(Call call, int i, ParameterNode p) {
* Holds if `arg` is a possible argument to `p` taking virtual dispatch into account.
*/
private predicate viableArgParam(ArgumentNode arg, ParameterNode p) {
exists(int i, Call call |
viableParam(call, i, p) and
exists(int i, DataFlowCall call |
viableParam(call.asCall(), i, p) and
arg.argumentOf(call, i)
)
}

View File

@@ -0,0 +1,100 @@
package my.callback.qltest;
public class A {
public interface Consumer1 {
void eat(Object o);
}
public interface Consumer2 {
void eat(Object o);
}
public interface Consumer3<T> {
void eat(T o);
}
static void applyConsumer1(Object x, Consumer1 con) {
// summary:
// con.eat(x);
}
static void applyConsumer2(Object x, Consumer2 con) {
// summary:
// con.eat(x);
}
static <T> void applyConsumer3(T x, Consumer3<T> con) {
// summary:
// con.eat(x);
}
public interface Producer1<T> {
T make();
}
static <T> T applyProducer1(Producer1<T> prod) {
// summary:
// return prod.make();
return null;
}
public interface Converter1<T1,T2> {
T2 conv(T1 x);
}
static <T1,T2> T2 applyConverter1(T1 x, Converter1<T1,T2> con) {
// summary:
// return con.conv(x);
return null;
}
static Object source(int i) { return null; }
static void sink(Object o) { }
void foo(boolean b1, boolean b2) {
applyConsumer1(source(1), p -> {
sink(p); // $ flow=1
});
Object handler;
if (b1) {
handler = (Consumer1)(p -> { sink(p); }); // $ flow=2
} else {
handler = (Consumer2)(p -> { sink(p); }); // $ flow=3
}
if (b2) {
applyConsumer1(source(2), (Consumer1)handler);
} else {
applyConsumer2(source(3), (Consumer2)handler);
}
applyConsumer1(source(4), new Consumer1() {
@Override public void eat(Object o) {
sink(o); // $ MISSING: flow=4
}
});
applyConsumer1(source(5), A::sink); // $ flow=5
Consumer2 c = new MyConsumer2();
applyConsumer2(source(6), c);
}
static class MyConsumer2 implements Consumer2 {
@Override public void eat(Object o) {
sink(o); // $ MISSING: flow=6
}
}
void foo2() {
Consumer3<Integer> c = i -> sink(i); // $ flow=7
applyConsumer3((Integer)source(7), c);
sink(applyProducer1(() -> (Integer)source(8))); // $ flow=8
sink(applyConverter1((Integer)source(9), i -> i)); // $ flow=9
sink(applyConverter1((Integer)source(10), i -> new int[]{i})[0]); // $ flow=10
}
}

View File

@@ -0,0 +1,45 @@
import java
import semmle.code.java.dataflow.DataFlow
import semmle.code.java.dataflow.ExternalFlow
import TestUtilities.InlineExpectationsTest
class SummaryModelTest extends SummaryModelCsv {
override predicate row(string row) {
row =
[
"my.callback.qltest;A;false;applyConsumer1;(Object,Consumer1);;Argument[0];Parameter[0] of Argument[1];value",
"my.callback.qltest;A;false;applyConsumer2;(Object,Consumer2);;Argument[0];Parameter[0] of Argument[1];value",
"my.callback.qltest;A;false;applyConsumer3;(Object,Consumer3);;Argument[0];Parameter[0] of Argument[1];value",
"my.callback.qltest;A;false;applyProducer1;(Producer1);;ReturnValue of Argument[0];ReturnValue;value",
"my.callback.qltest;A;false;applyConverter1;(Object,Converter1);;Argument[0];Parameter[0] of Argument[1];value",
"my.callback.qltest;A;false;applyConverter1;(Object,Converter1);;ReturnValue of Argument[1];ReturnValue;value"
]
}
}
class Conf extends DataFlow::Configuration {
Conf() { this = "qltest:callback-dispatch" }
override predicate isSource(DataFlow::Node n) {
n.asExpr().(MethodAccess).getMethod().hasName("source")
}
override predicate isSink(DataFlow::Node n) {
exists(MethodAccess ma | ma.getMethod().hasName("sink") | n.asExpr() = ma.getAnArgument())
}
}
class HasFlowTest extends InlineExpectationsTest {
HasFlowTest() { this = "HasFlowTest" }
override string getARelevantTag() { result = "flow" }
override predicate hasActualResult(Location location, string element, string tag, string value) {
tag = "flow" and
exists(DataFlow::Node src, DataFlow::Node sink, Conf conf | conf.hasFlow(src, sink) |
sink.getLocation() = location and
element = sink.toString() and
value = src.asExpr().(MethodAccess).getAnArgument().toString()
)
}
}