Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Redefine method signatures of methods that are called from another compiled method

Tags:

java

javassist

I am trying to replace a method with a different method that has a different return type but it seems like the only way I can successfully do it is to have the source code of all methods that have a call site to the replaced method. I have even tried instrumenting the calling methods to call the new replacement method but I get a VerifyError (Bad type on operand stack). Is there a way to only use the bytecode and not depend on the source code when rebuilding dependent methods?

Functioning Example (with source code dependency)

public class OverrideTest {  
    final ClassPool POOL = ClassPool.getDefault();  

    class Foo {  
        public Integer getFoo() { return new Integer(1); }  
        public String doA() { return getFoo().toString(); }  
    }  

    class FooReplacement {  
        public String getFoo() { return "A"; }  
    }  

    @Test  
    public void d() throws Throwable {  
        CtClass srcClass = POOL.getCtClass(Foo.class.getName());  
        CtClass extClass = POOL.getCtClass(FooReplacement.class.getName());  
        CtClass d = POOL.makeClass("Derp");  
        CtClass STRING = POOL.get("java.lang.String");  
        CtClass INT = POOL.get("java.lang.Integer");  
        {  
            CtMethod doA1 = srcClass.getMethod("doA", Descriptor.ofMethod(STRING, new CtClass[0]));  
            CtMethod getFoo1 = srcClass.getMethod("getFoo", Descriptor.ofMethod(INT, new CtClass[0]));  
            CtMethod getFoo = new CtMethod(INT, "getFoo", new CtClass[0], d);  
            CtMethod doA = new CtMethod(STRING, "doA", new CtClass[0], d);  
            d.addMethod(doA);  
            d.addMethod(getFoo);  
            doA.setBody(doA1, null);  
            getFoo.setBody(getFoo1, null);  
            d.setModifiers(d.getModifiers() & ~Modifier.ABSTRACT);  

            d.removeMethod(getFoo);  
            CtMethod getFooReplaced = new CtMethod(STRING, "getFoo", new CtClass[0], d);  
            d.addMethod(getFooReplaced);  
            CtMethod getFooReplaced1 = extClass.getMethod("getFoo", Descriptor.ofMethod(STRING, new CtClass[0]));  
            getFooReplaced.setBody(getFooReplaced1, null);  

            doA.setBody("{ return getFoo().toString(); }");  
            d.setModifiers(d.getModifiers() & ~Modifier.ABSTRACT);  
        }  
        {  
            Class<?> c = d.toClass();  
            Constructor<?> ctor = c.getConstructor();  
            Object derp = ctor.newInstance();  
            Method getFoo = derp.getClass().getMethod("getFoo");  
            Method doA = derp.getClass().getMethod("doA");  
            Object doResult = doA.invoke(derp);  
            Object getResult = getFoo.invoke(derp);  
            assertEquals("A", getResult);  
            assertEquals("A", doResult);  
        }  
    }  
}  

Non-Functioning Example (VerifyError)

public class OverrideTest {  
    final ClassPool POOL = ClassPool.getDefault();  


    class Foo {  
        public Integer getFoo() { return new Integer(1); }  
        public String doA() { return getFoo().toString(); }  
    }  


    class FooReplacement {  
        public String getFoo() { return "A"; }  
    }  


    @Test  
    public void d() throws Throwable {  
        CtClass srcClass = POOL.getCtClass(Foo.class.getName());  
        CtClass extClass = POOL.getCtClass(FooReplacement.class.getName());  
        CtClass d = POOL.makeClass("Derp");  
        CtClass STRING = POOL.get("java.lang.String");  
        CtClass INT = POOL.get("java.lang.Integer");  
        {  
            CtMethod doA1 = srcClass.getMethod("doA", Descriptor.ofMethod(STRING, new CtClass[0]));  
            CtMethod getFoo1 = srcClass.getMethod("getFoo", Descriptor.ofMethod(INT, new CtClass[0]));  
            CtMethod getFoo = new CtMethod(INT, "getFoo", new CtClass[0], d);  
            CtMethod doA = new CtMethod(STRING, "doA", new CtClass[0], d);  
            d.addMethod(doA);  
            d.addMethod(getFoo);  
            doA.setBody(doA1, null);  
            getFoo.setBody(getFoo1, null);  
            d.setModifiers(d.getModifiers() & ~Modifier.ABSTRACT);  


            CtMethod tempMethod = new CtMethod(getFoo.getReturnType(), "tempFoo", new CtClass[0], d);  
            d.addMethod(tempMethod);  
            doA.instrument(new MethodReplacer(getFoo, tempMethod));  
            d.removeMethod(getFoo);  


            CtMethod getFooReplaced = new CtMethod(STRING, "getFoo", new CtClass[0], d);  
            d.addMethod(getFooReplaced);  
            CtMethod getFooReplaced1 = extClass.getMethod("getFoo", Descriptor.ofMethod(STRING, new CtClass[0]));  
            getFooReplaced.setBody(getFooReplaced1, null);  


            doA.instrument(new MethodReplacer(tempMethod, getFooReplaced));  
            d.removeMethod(tempMethod);  
            d.removeMethod(doA);  
            CtMethod doA2 = new CtMethod(STRING, "doA", new CtClass[0], d);  
            d.addMethod(doA2);  
            doA2.setBody(doA, null);  
            d.setModifiers(d.getModifiers() & ~Modifier.ABSTRACT);  
        }  
        {  
            Class<?> c = d.toClass();  
            Constructor<?> ctor = c.getConstructor();  
            Object derp = ctor.newInstance();  
            Method getFoo = derp.getClass().getMethod("getFoo");  
            Method doA = derp.getClass().getMethod("doA");  
            Object doResult = doA.invoke(derp);  
            Object getResult = getFoo.invoke(derp);  
            assertEquals("A", getResult);  
            assertEquals("A", doResult);  
        }  
    }  


    class MethodReplacer extends ExprEditor {  
        private CtMethod replacedMethod;  
        private CtMethod replacement;  


        MethodReplacer(CtMethod replacedMethod, CtMethod replacement) {  
            this.replacedMethod = replacedMethod;  
            this.replacement = replacement;  
        }  


        @Override  
        public void edit(MethodCall mcall) throws CannotCompileException {  
            CtClass declaringClass = replacedMethod.getDeclaringClass();  
            try {  
                CtMethod m = mcall.getMethod();  
                if (declaringClass.equals(m.getDeclaringClass()) && m.equals(replacedMethod))  
                    mcall.replace("$_ = " + replacement.getName()+"($$);");  
            } catch (NotFoundException e) {  
                throw new RuntimeException("Unable to instrument a dependent method call to " + replacedMethod.getName(), e);  
            }  
        }  


    }  
}  
like image 903
Aaron Avatar asked Nov 11 '22 10:11

Aaron


1 Answers

When you compile a class with a single method:

class Foo {

  Object bar() { }
}

this method's descriptor which is part of the method's signature will look something like this after compilation:

()Ljava/lang/Object;

Note that the return type is part of the method signature! Thus, any call to the method is made explicitly to a method that returns an instance of type Object. If you changes the return type to for example an Integer, the descriptor would instead be ()Ljava/lang/Integer;.

If you had compiled a class Qux to call the method of Foo with the ()Ljava/lang/Object; descriptor but then changed the method in Foo to return an Integer instead, the method call could not be dispatched by the JVM without recompiling Qux: For the JVM, the method Qux is calling does no longer exist. The same issue applied for another method defined in Foo when you use a tool like javassist to only "recompile" a single method without recompiling methods that call the first method. This is the problem the verifier complains about in your example.

However, the Java compiler knows the concept of bridge methods for this purpose where Foo would look like in a pseudo Java code that represents the generated Java byte code.

class Foo {

  Object bar() { this.[Integer]bar(); }

  Integer bar() { }
}

after its compilation where this.[Integer]bar(); represents a call to the bar method that returns an Integer. The [Object]bar() method represents a bridge method in this context. You could emulate the creation of such a bridge method yourself by first redefining the method signature to return a different type and additionally adding a bridge method with the original return type.

like image 104
Rafael Winterhalter Avatar answered Nov 14 '22 21:11

Rafael Winterhalter