javabytecodejava-bytecode-asmbyte-buddyverifyerror

VerifyException when adding an if stmt to a method at runtime


Let's say I have a simple

class C$Manipulatables{
    public Wrapper get(long v, String f){
        if(v == 0){
            return new Wrapper(0);
        }else{
            throw new RuntimeException("unacceptable: v not found");
        }
    }
}

I now want to redefine the get in C$Manipulatables, s.t. it reads

class C$Manipulatables{
    public Wrapper get(long v, String f){
        if(v == 1){ return null; }
        if(v == 0){
            return new Wrapper(0);
        }else{
            throw new RuntimeException("unacceptable: v not found");
        }
    }
}

This will trigger a nullpointer exception if I try to use the new value, but that's ok - for now I just want confirmation that the new code is loaded.

We add the code with ASM (I will add the full copy-pastable code at the bottom of this post, so I'm just zooming in on the relevant parts, here):

    class AddingMethodVisitor extends MethodVisitor implements Opcodes{
        int v;
        public AddingMethodVisitor(int v, int api, MethodVisitor mv) {
            super(api, mv);
            this.v = v;
        }

        @Override
        public void visitCode() {
            super.visitCode();

            mv.visitVarInsn(LLOAD, 1); //arg 1 of method is version number

            /*if arg1 == the new version*/
            mv.visitLdcInsn(v);
            Label lSkip = new Label();

            mv.visitInsn(LCMP);
            mv.visitJumpInsn(IFNE, lSkip);

            mv.visitInsn(ACONST_NULL);
            mv.visitInsn(ARETURN);

            /*else*/
            mv.visitLabel(lSkip);
            mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);

        }
    }

and reload the class with ByteBuddy (again, full code at the bottom of the post):

        ClassReader cr;
        try {
            /*note to self: don't forget the ``.getClassLoader()``*/
            cr = new ClassReader(manipsClass.getClassLoader().getResourceAsStream( manipsClass.getName().replace('.','/') + ".class"));
        }catch(IOException e){
            throw new RuntimeException(e);
        }

        ClassWriter cw = new ClassWriter(cr, 0);

        VersionAdder adder = new VersionAdder(C.latest + 1,Opcodes.ASM5,cw);

        cr.accept(adder,0);

        System.out.println("reloading C$Manipulatables class");
        byte[] bytes = cw.toByteArray();

        ClassFileLocator classFileLocator = ClassFileLocator.Simple.of(manipsClass.getName(), bytes);

        new ByteBuddy()
                .redefine(manipsClass,classFileLocator)
                .name(manipsClass.getName())
                .make()
                .load(C.class.getClassLoader(), ClassReloadingStrategy.fromInstalledAgent())
        ;

        C.latest++;
        System.out.println("RELOADED");
    }
}

This fails.

got 0
reloading C$Manipulatables class
Exception in thread "main" java.lang.VerifyError
    at sun.instrument.InstrumentationImpl.redefineClasses0(Native Method)
    at sun.instrument.InstrumentationImpl.redefineClasses(InstrumentationImpl.java:170)
    at net.bytebuddy.dynamic.loading.ClassReloadingStrategy$Strategy$1.apply(ClassReloadingStrategy.java:261)
    at net.bytebuddy.dynamic.loading.ClassReloadingStrategy.load(ClassReloadingStrategy.java:171)
    at net.bytebuddy.dynamic.TypeResolutionStrategy$Passive.initialize(TypeResolutionStrategy.java:79)
    at net.bytebuddy.dynamic.DynamicType$Default$Unloaded.load(DynamicType.java:4456)
    at redefineconcept.CInserter.addNext(CInserter.java:60)
    at redefineconcept.CInserter.run(CInserter.java:22)
    at redefineconcept.CInserter.main(CInserter.java:16)

Process finished with exit code 1

In fact, this even fails when I comment the return null stmt generation (as I have in the full code provided below).

Apparently, java just doesn't like the way I've constructed my IF, even though it's essentially the code I got when I used asmifier on

public class B {

    public Object run(long version, String field){
        if(version == 2) {
            return null;
        }
        return null;
    }
}

which yielded

{
mv = cw.visitMethod(ACC_PUBLIC, "run", "(JLjava/lang/String;)Ljava/lang/Object;", null, null);
mv.visitCode();
mv.visitVarInsn(LLOAD, 1);
mv.visitLdcInsn(new Long(2L));
mv.visitInsn(LCMP);
Label l0 = new Label();
mv.visitJumpInsn(IFNE, l0);
mv.visitInsn(ACONST_NULL);
mv.visitInsn(ARETURN);
mv.visitLabel(l0);
mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
mv.visitInsn(ACONST_NULL);
mv.visitInsn(ARETURN);
mv.visitMaxs(4, 4);
mv.visitEnd();
}

I simply left away everything after

mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);

because after that, the already existing part of the method follows.

I really do believe that there's something about that visitFrame that java doesn't like.

Let's say I change my visitCode s.t. it reads

        public void visitCode() {
            super.visitCode();

            mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
            mv.visitLdcInsn("Work, you ..!");
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);

//            mv.visitVarInsn(LLOAD, 1); //arg 1 of method is version number
//
//            /*if arg1 == the new version*/
//            mv.visitLdcInsn(v);
//            Label lSkip = new Label();
//
//            mv.visitInsn(LCMP);
//            mv.visitJumpInsn(IFNE, lSkip);
//
////            mv.visitInsn(ACONST_NULL);
////            mv.visitInsn(ARETURN);
//
//            /*else*/
//            mv.visitLabel(lSkip);
//            mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);

        }

Then the redefinition works. I get the expected exception because the code falls through to the original if-else block that cannot handle the new version number, but I do get the output, at least.

got 0
reloading C$Manipulatables class
RELOADED
Work, you ..!
Exception in thread "main" java.lang.RuntimeException: unacceptable: v not found
    at redefineconcept.C$Manipulatables.get(C.java:27)
    at redefineconcept.C.get(C.java:10)
    at redefineconcept.CInserter.run(CInserter.java:23)
    at redefineconcept.CInserter.main(CInserter.java:16)

I'd very much appreciate some help in resolving this. What's the correct way to insert a new if stmt that java will accept?

FULL CODE

C.java

(Note that the class C$Manipulatables is necessary, because ByteBuddy cannot redefine classes that have static initialisers.)

package redefineconcept;

public class C {
    public static volatile int latest = 0;

    public static final C$Manipulatables manips = new C$Manipulatables();

    public int get(){
        int v = latest;
        return manips.get(v,"").value;
    }
}

class Wrapper{
    int value;

    public Wrapper(int value){
        this.value = value;
    }
}

class C$Manipulatables{
    public Wrapper get(long v, String f){
        if(v == 0){
            return new Wrapper(0);
        }else{
            throw new RuntimeException("unacceptable: v not found");
        }
    }
}

CInserter.java

package redefineconcept;

import net.bytebuddy.ByteBuddy;
import net.bytebuddy.agent.ByteBuddyAgent;
import net.bytebuddy.dynamic.ClassFileLocator;
import net.bytebuddy.dynamic.loading.ClassReloadingStrategy;
import net.bytebuddy.jar.asm.*;

import java.io.IOException;

    public class CInserter {
        public static void main(String[] args) {

            ByteBuddyAgent.install();

            new CInserter().run();
        }

        private void run(){
            C c = new C();
            System.out.println("got " + c.get());
            addNext();
            System.out.println("got " + c.get()); //should trigger nullptr exception
        }

        private void addNext(){

            Object manips;
            String manipsFld = "manips";

            try {
                manips = C.class.getDeclaredField(manipsFld).get(null);
            }catch(NoSuchFieldException | IllegalAccessException e){
                throw new RuntimeException(e);
            }

            Class<?> manipsClass = manips.getClass();
            assert(manipsClass.getName().equals("redefineconcept.C$Manipulatables"));


            ClassReader cr;
            try {
                /*note to self: don't forget the ``.getClassLoader()``*/
                cr = new ClassReader(manipsClass.getClassLoader().getResourceAsStream( manipsClass.getName().replace('.','/') + ".class"));
            }catch(IOException e){
                throw new RuntimeException(e);
            }

            ClassWriter cw = new ClassWriter(cr, 0);

            VersionAdder adder = new VersionAdder(C.latest + 1,Opcodes.ASM5,cw);

            cr.accept(adder,0);

            System.out.println("reloading C$Manipulatables class");
            byte[] bytes = cw.toByteArray();

            ClassFileLocator classFileLocator = ClassFileLocator.Simple.of(manipsClass.getName(), bytes);

            new ByteBuddy()
                    .redefine(manipsClass,classFileLocator)
                    .name(manipsClass.getName())
                    .make()
                    .load(C.class.getClassLoader(), ClassReloadingStrategy.fromInstalledAgent())
            ;

            C.latest++;
            System.out.println("RELOADED");
        }
    }

class VersionAdder extends ClassVisitor{
    private int v;
    public VersionAdder(int v, int api, ClassVisitor cv) {
        super(api, cv);
        this.v = v;
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);

        if(mv != null && name.equals("get")){
            return new AddingMethodVisitor(v,Opcodes.ASM5,mv);
        }

        return mv;
    }

    class AddingMethodVisitor extends MethodVisitor implements Opcodes{
        int v;
        public AddingMethodVisitor(int v, int api, MethodVisitor mv) {
            super(api, mv);
            this.v = v;
        }

        @Override
        public void visitCode() {
            super.visitCode();

            mv.visitVarInsn(LLOAD, 1); //arg 1 of method is version number

            /*if arg1 == the new version*/
            mv.visitLdcInsn(v);
            Label lSkip = new Label();

            mv.visitInsn(LCMP);
            mv.visitJumpInsn(IFNE, lSkip);

//            mv.visitInsn(ACONST_NULL);
//            mv.visitInsn(ARETURN);

            /*else*/
            mv.visitLabel(lSkip);
            mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);

        }
    }
}

Solution

  • One bug I noticed in your code is the following line

    mv.visitLdcInsn(v);

    The intent of the code is to create and load a long constant, but v has type int, so an integer constant will be created instead, thus creating a type error in the bytecode when you compare it with lcmp on the next line. visitLdcInsn will create a different constant type depending on the type of Object you pass in, so the argument needs to be the exact type you want.

    On a side note, you don't need LDC in the first place to create a long constant of value 1, because there is a dedicated bytecode instruction for this, lconst_1. In ASM, this should be something like visitInsn(LCONST_1);