-
Notifications
You must be signed in to change notification settings - Fork 177
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
In the call graph, the call edges related to dynamic proxy are missing. #123
Comments
Thank you for taking the time to provide such detailed information. This seems to be a rather important issue, we'll take the time to look into it after being free. Before we investigate this issue further, we would like to conduct a user study to understand your experience with our GitHub Issue Template. Specifically, we want to determine if there are any organizational, descriptive or structural aspects of the template that make it difficult/undesirable for you to follow when submitting an issue. |
I apologize for not strictly adhering to the issue template format when submitting my issue. I’d like to explain the reason behind this. When describing my example in the Overall Description, whether it’s for this issue or previous ones, I find it difficult to separate the Expected Behavior and Current Behavior from the Overall Description. When describing the issue, I always feel that placing Expected Behavior and Current Behavior as separate headings after the Overall Description creates a sense of “disconnection.” It feels like it disrupts the flow of the explanation. Taking this submission as an example, I want to analyze the function calls related to dynamic proxies. I first provided a brief description in the title: “call edges related to dynamic proxy are missing.” Then, in the Overall Description, I started by offering a demo as a sample for analysis.
Afterward, I presented the resulting call graph and explained the outcome of this analysis.
Next, I described the actual runtime call sequence:
In this process: ① is the Reproducible Example ② is the Current Behavior ③ is the Expected Behavior(Perhaps I didn’t describe it clearly enough. I should have included a call chain like: If I strictly followed the template, the structure would probably look like this: I would first describe the issue in the Overall Description, then follow with either a ③②① or ②③① format. Personally, I believe that describing the entire process directly in the Overall Description makes it easier to follow and understand. Therefore, I placed everything in the Description section. In this case, if I were to follow the template strictly, it would result in redundant content. That’s why I filled in “None” for both Expected Behavior and Current Behavior. In fact, to ensure that others could understand more easily, I revised the content and format multiple times before submitting. (However, looking at it again now, it seems I should have used symbols like “·” or “>” to better organize the structure.) Regarding the issue template, I personally believe that Expected Behavior and Current Behavior could be subheadings under the Overall Description, but this is just my personal opinion. You may want to gather feedback from other users to make a more informed decision. |
Hi YunFy26, I set the value of As for the question, |
@BryanHeBY Apologies for mistakenly assuming that the value of In this repo, after running java -Djdk.proxy.ProxyGenerator.saveGeneratedFiles=true -cp . org.example.Main This caused the bytecode file of the dynamic proxy class to be saved in This is unrelated to the missing call edges in method invocations within dynamic proxy classes. I apologize for my limited expertise, which may have caused inconvenience to the Tai-e team members. I also sincerely appreciate the Tai-e team for addressing my questions. |
Tai-e currently does not support handling dynamic proxy. Dynamic proxy generate the bytecode for the proxy class as However, the behavior of dynamic proxy is not complex. A proxy class is generated based on the input interfaces. It holds an Here, I can provide you with two methods for reference: Method 1After generating the proxy classes files (.class file) in the runtime (using Plugin code
public class CustomModel extends CompositePlugin {
@Override
public void setSolver(Solver solver) {
addPlugin(new IRProxyModel(solver));
}
}
public class IRProxyModel extends IRModelPlugin {
private final Set<JClass> proxyClasses;
private final Type invocationHandlerType;
IRProxyModel(Solver solver) {
super(solver);
proxyClasses = getProxyClasses();
invocationHandlerType = typeSystem.getClassType("java.lang.reflect.InvocationHandler");
}
private Set<JClass> getProxyClasses() {
JClass proxy = hierarchy.getClass("java.lang.reflect.Proxy");
String regex = "jdk\\.proxy\\d+\\.\\$Proxy\\d+";
Predicate<String> pattern = Pattern.compile(regex).asMatchPredicate();
return hierarchy.getAllSubclassesOf(proxy).stream()
.filter(c -> pattern.test(c.getName()))
.collect(Collectors.toSet());
}
@InvokeHandler(signature = "<java.lang.reflect.Proxy: java.lang.Object newProxyInstance(java.lang.ClassLoader,java.lang.Class[],java.lang.reflect.InvocationHandler)>")
public List<Stmt> newProxyInstance(Invoke invoke) {
List<Stmt> stmts = new ArrayList<>();
Var result = invoke.getResult();
Var invocationHandler = invoke.getInvokeExp().getArg(2);
if (result != null) {
for (var proxy : proxyClasses) {
for (var method : proxy.getDeclaredMethods()) {
if (!method.isConstructor()) {
continue;
}
if (method.getParamCount() == 1 && method.getParamType(0).equals(invocationHandlerType)) {
stmts.add(new New(invoke.getContainer(), result, new NewInstance(proxy.getType())));
stmts.add(new Invoke(invoke.getContainer(),
new InvokeSpecial(method.getRef(), result, List.of(invocationHandler))));
}
}
}
}
return stmts;
}
} You need to activate the plugin by declaring it in the configuration file. You also need to enable the reflection analysis (because dynamic proxy will use reflection) and disable the Partial call graph...
"122" [label="<MyInvocationHandler: java.lang.Object invoke(java.lang.Object,java.lang.reflect.Method,java.lang.Object[])>",];
"13887" [label="<jdk.proxy1.$Proxy0: void doSomething()>",];
"14293" [label="<Main: void main(java.lang.String[])>",];
"14857" [label="<ServiceImpl: void doSomething()>",];
...
"14293" -> "13887" [label="[4@L8] invokeinterface r2.<Service: void doSomething()>();",];
"13887" -> "122" [label="[2@L-1] invokeinterface $r2.<java.lang.reflect.InvocationHandler: java.lang.Object invoke(java.lang.Object,java.lang.reflect.Method,java.lang.Object[])>(%this, $r1, %nullconst);",];
"122" -> "14857" [label="[4@L17] $r5 = invokevirtual method.<java.lang.reflect.Method: java.lang.Object invoke(java.lang.Object,java.lang.Object[])>($r4, args);",];
... The plugin models This method requires running the program in advance to generate all proxy classes, which is not that 'static'. Method 2Since the logic for generating dynamic proxy code is not that difficult—requiring the proxy class to directly delegate to the Plugin code
public class CustomModel extends CompositePlugin {
@Override
public void setSolver(Solver solver) {
addPlugin(new SemanticProxyModel(solver));
}
}
public class SemanticProxyModel extends AnalysisModelPlugin {
private static final Logger logger = LogManager.getLogger(SemanticProxyModel.class);
private static final Descriptor PROXY_DESC = () -> "ProxyObj";
private static final Descriptor REFLECTION_DESC = () -> "ReflectionMetaObj";
private static final Descriptor ARGS_ARRAY_DESC = () -> "Object[Args]Obj";
private final JClass object;
private final JClass proxy;
private final Set<MethodRef> proxiedMethods;
SemanticProxyModel(Solver solver) {
super(solver);
object = Objects.requireNonNull(hierarchy.getJREClass(ClassNames.OBJECT));
proxy = Objects.requireNonNull(hierarchy.getJREClass("java.lang.reflect.Proxy"));
proxiedMethods = Set.of(
Objects.requireNonNull(object.getDeclaredMethod("hashCode")).getRef(),
Objects.requireNonNull(object.getDeclaredMethod("equals")).getRef(),
Objects.requireNonNull(object.getDeclaredMethod("toString")).getRef());
}
@Override
public void onStart() {
handlers.keySet().forEach(solver::addIgnoredMethod);
}
@InvokeHandler(signature = "<java.lang.reflect.Proxy: java.lang.Object newProxyInstance(java.lang.ClassLoader,java.lang.Class[],java.lang.reflect.InvocationHandler)>", argIndexes = {2})
public void newProxyInstance(Context context, Invoke invoke, PointsToSet invocationHandler) {
Var result = invoke.getResult();
if (result != null) {
invocationHandler.forEach(
csObj -> {
// generate special mock object for newProxyInstance
Obj obj = heapModel.getMockObj(PROXY_DESC, csObj, NullType.NULL);
CSMethod csMethod = csManager.getCSMethod(context, invoke.getContainer());
Context heapContext = selector.selectHeapContext(csMethod, obj);
solver.addVarPointsTo(context, result, heapContext, obj);
}
);
}
}
@Override
public void onUnresolvedCall(CSObj recv, Context context, Invoke invoke) {
if (!CSObjs.hasDescriptor(recv, PROXY_DESC)) {
return;
}
MethodRef method = invoke.getMethodRef();
JClass clazz = method.getDeclaringClass();
if (clazz.equals(proxy) || (clazz.equals(object) && !proxiedMethods.contains(method))) {
// the method is directly called
CSCallSite csCallSite = csManager.getCSCallSite(context, invoke);
JMethod callee = method.resolve();
Context calleeContext = selector.selectContext(
csCallSite, recv, callee);
CSMethod csCallee = csManager.getCSMethod(calleeContext, callee);
solver.addCallEdge(new Edge<>(CallGraphs.getCallKind(invoke),
csCallSite, csCallee));
solver.addVarPointsTo(calleeContext, callee.getIR().getThis(), recv);
} else {
// the method is actually delegated to InvocationHandler.invoke method
CSObj invocationHandler = (CSObj) recv.getObject().getAllocation();
JMethod callee = ((ClassType) invocationHandler.getObject().getType())
.getJClass().getDeclaredMethod("invoke");
if (callee == null) {
logger.warn("No invoke method for " + invocationHandler.getObject().getType());
return;
}
CSCallSite csCallSite = csManager.getCSCallSite(context, invoke);
Context calleeContext = selector.selectContext(
csCallSite, invocationHandler, callee);
CSMethod csCallee = csManager.getCSMethod(calleeContext, callee);
solver.addCallEdge(new ProxyCallEdge(csCallSite, csCallee, recv));
solver.addVarPointsTo(calleeContext, callee.getIR().getThis(),
invocationHandler);
}
}
@Override
public void onNewCallEdge(Edge<CSCallSite, CSMethod> edge) {
if (edge instanceof ProxyCallEdge proxyEdge) {
// create arguments for InvocationHandler.invoke
CSMethod csCallee = edge.getCallee();
Context callerCtx = edge.getCallSite().getContext();
Invoke callSite = edge.getCallSite().getCallSite();
Context calleeCtx = csCallee.getContext();
JMethod callee = csCallee.getMethod();
InvokeExp invokeExp = callSite.getInvokeExp();
// pass the first argument, which is reflection method
solver.addVarPointsTo(callerCtx, callee.getIR().getParam(0), proxyEdge.getProxyObj());
// pass the second argument, which is reflection method
JMethod method = callSite.getMethodRef().resolve();
Obj methodObj = heapModel.getMockObj(REFLECTION_DESC, method, typeSystem.getClassType(ClassNames.METHOD));
Context mObjContext = selector.selectHeapContext(proxyEdge.getCallee(), methodObj);
solver.addVarPointsTo(callerCtx, callee.getIR().getParam(1), mObjContext, methodObj);
// pass the third argument, which is args in Object[]
Type objs = typeSystem.getArrayType(typeSystem.getType(ClassNames.OBJECT), 1);
Obj argsObj = heapModel.getMockObj(ARGS_ARRAY_DESC, callSite, objs);
Context argsObjContext = selector.selectHeapContext(proxyEdge.getCallee(), argsObj);
ArrayIndex arrayIdx = csManager.getArrayIndex(csManager.getCSObj(argsObjContext, argsObj));
callSite.getInvokeExp().getArgs().forEach(
v -> {
CSVar csVar = csManager.getCSVar(callerCtx, v);
PointsToSet pts = solver.getPointsToSetOf(csVar);
solver.addPointsTo(arrayIdx, pts);
}
);
solver.addVarPointsTo(callerCtx, callee.getIR().getParam(2), argsObjContext, argsObj);
// pass results to LHS variable
Var lhs = callSite.getResult();
if (lhs != null) {
CSVar csLHS = csManager.getCSVar(callerCtx, lhs);
for (Var ret : callee.getIR().getReturnVars()) {
CSVar csRet = csManager.getCSVar(calleeCtx, ret);
solver.addPFGEdge(csRet, csLHS, FlowKind.RETURN);
}
}
}
}
}
public class ProxyCallEdge extends OtherEdge<CSCallSite, CSMethod> {
private final CSObj proxyObj;
public ProxyCallEdge(CSCallSite callSite, CSMethod callee, CSObj proxyObj) {
super(callSite, callee);
this.proxyObj = proxyObj;
}
public CSObj getProxyObj() {
return proxyObj;
}
} You need to activate the plugin by declaring it in the configuration file. You also need to enable the reflection analysis (because dynamic proxy will use reflection). You can set the Call graphdigraph G {
node [color=".3 .2 1.0",shape=box,style=filled];
edge [];
"0" [label="<MyInvocationHandler: java.lang.Object getProxy(java.lang.Object)>",];
"1" [label="<java.lang.String: void <clinit>()>",];
"2" [label="<java.lang.Object: void <init>()>",];
"3" [label="<java.lang.Class: java.lang.ClassLoader getClassLoader()>",];
"4" [label="<java.lang.reflect.Method: java.lang.Object invoke(java.lang.Object,java.lang.Object[])>",];
"5" [label="<java.lang.reflect.Proxy: java.lang.Object newProxyInstance(java.lang.ClassLoader,java.lang.Class[],java.lang.reflect.InvocationHandler)>",];
"6" [label="<java.lang.Class: java.lang.Class[] getInterfaces()>",];
"7" [label="<ServiceImpl: void doSomething()>",];
"8" [label="<Main: void main(java.lang.String[])>",];
"9" [label="<MyInvocationHandler: java.lang.Object invoke(java.lang.Object,java.lang.reflect.Method,java.lang.Object[])>",];
"10" [label="<ServiceImpl: void <init>()>",];
"11" [label="<java.lang.System: void <clinit>()>",];
"12" [label="<MyInvocationHandler: void <init>(java.lang.Object)>",];
"13" [label="<java.lang.Object: java.lang.Class getClass()>",];
"0" -> "13" [label="[0@L24] $r1 = invokevirtual r0.<java.lang.Object: java.lang.Class getClass()>();",];
"0" -> "3" [label="[1@L24] $r2 = invokevirtual $r1.<java.lang.Class: java.lang.ClassLoader getClassLoader()>();",];
"0" -> "12" [label="[5@L25] invokespecial $r5.<MyInvocationHandler: void <init>(java.lang.Object)>(r0);",];
"0" -> "6" [label="[3@L25] $r4 = invokevirtual $r3.<java.lang.Class: java.lang.Class[] getInterfaces()>();",];
"0" -> "13" [label="[2@L25] $r3 = invokevirtual r0.<java.lang.Object: java.lang.Class getClass()>();",];
"0" -> "5" [label="[6@L23] $r6 = invokestatic <java.lang.reflect.Proxy: java.lang.Object newProxyInstance(java.lang.ClassLoader,java.lang.Class[],java.lang.reflect.InvocationHandler)>($r2, $r4, $r5);",];
"8" -> "0" [label="[2@L7] $r1 = invokestatic <MyInvocationHandler: java.lang.Object getProxy(java.lang.Object)>($r0);",];
"8" -> "10" [label="[1@L6] invokespecial $r0.<ServiceImpl: void <init>()>();",];
"8" -> "9" [label="[4@L8] invokeinterface r2.<Service: void doSomething()>();",];
"9" -> "7" [label="[4@L17] $r5 = invokevirtual method.<java.lang.reflect.Method: java.lang.Object invoke(java.lang.Object,java.lang.Object[])>($r4, args);",];
"9" -> "4" [label="[4@L17] $r5 = invokevirtual method.<java.lang.reflect.Method: java.lang.Object invoke(java.lang.Object,java.lang.Object[])>($r4, args);",];
"10" -> "2" [label="[0@L1] invokespecial %this.<java.lang.Object: void <init>()>();",];
"12" -> "2" [label="[0@L9] invokespecial %this.<java.lang.Object: void <init>()>();",];
}
This plugin models In reality, the object of the proxy class is a subclass that implements the proxied interfaces. To further improve this plugin, you can specially handle the propagation of the object through the interfaces parameter when calling the As for the question, |
Thank you for providing such a detailed solution, and apologies for the delayed response. I’ll proceed with handling the situation based on your suggestions. Also, I must add, Tai-e is truly a powerful and user-friendly analysis framework! |
📝 Overall Description
### For the following demo
Service.java
ServiceImpl.java
MyInvocationHandler.java
Main.java
IR
ofMain.java
The
call-graph
as follows:The call edge
main
→doSomething
is missing.In the actual runtime call sequence, before
doSomething
is called, the methodinvoke
ofMyInvocationHandler
will be called, and thendoSomething
is called through reflection within theinvoke
method.After completing the pointer analysis, I reviewed the results of the analysis.
solver.csManager.callSites
includes:solver.csManager.ptrManager.vars.map
includes varr2
,but the pointsToSet ofr2
isnull
, As shown in Figure-1At runtime, the type of
r2
isjdk.proxy1.$Proxy0
Since
$Proxy0
is generated at runtime, Tai-e is unable to identify the allocation site for this object. So there is no Object mocked, which results in the missing call edge. Is my understanding correct?According to #114 :
Does this imply that Tai-e does not yet natively support method calls in dynamic proxy? If Tai-e supports handling method calls within proxy classes, what configurations should I modify?
Moreover, I have observed that
solver.csManager.objManager.objMap
contains:(as shown in Figure-2)Why is
org.example.proxy.ServiceImpl.class
considered aConstantObj
?Additionally, in
tai-e-analyses.yml
, I set the value ofhandle-invokedynamic
totrue
. Tai-e output the IR of$Proxy
:I have a few questions regarding this IR. Could you explain why the line number is shown as
-1
?🎯 Expected Behavior
None
🐛 Current Behavior
None
🔄 Reproducible Example
No response
⚙️ Tai-e Arguments
🔍 Click here to see Tai-e Options
🔍 Click here to see Tai-e Analysis Plan
📜 Tai-e Log
🔍 Click here to see Tai-e Log
ℹ️ Additional Information
No response
The text was updated successfully, but these errors were encountered: