简体   繁体   中英

Is it possible to loop infinitely with a switch statement?

I am writing a code quality tool. I am scanning source and compiled classes searching for potential infinite loops.

I can't think a way of a souce code switch statement can loop indefinitely. Am I wrong?

Switch statements compile to lookupswitch and tableswitch opcodes. I will need to check compile classes for security reasons, and also bytecode modifications are allow before the quality control program process the compiled classes. Having say that, is there a possible way of looping infinitely by using only those opcodes by modifying a class or generating it with assembler?

I have already taken care of every other branching instructions and statements.

Your help will be really appreciated.

Edit: Conclusion:

As I suspected and by the answers here provided, a switch statement in source code can only branch forward, but any branching instruction in bytecode could potentially jump backwards (assuming bytecode modifications).

It's interesting that you can do this with bytecode version 1.6 (50), but cannot with bytecode version 1.7 (51) as verification fails. This code (requires ASM5) works correctly and has infinite loop:

import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import static org.objectweb.asm.Opcodes.*;

public class LookupTest {
    public static void main(String[] args) throws InstantiationException,
            IllegalAccessException, ClassNotFoundException {
        new ClassLoader() {
            @Override
            protected Class<?> findClass(String name)
                    throws ClassNotFoundException {
                ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS
                        | ClassWriter.COMPUTE_FRAMES);
                // Create public class extending java.lang.Object
                cw.visit(V1_6, ACC_PUBLIC | ACC_SUPER, name, null,
                        "java/lang/Object", null);
                // Create default constructor
                MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V",
                        null, null);
                mv.visitCode();
                // Call superclass constructor (this is required)
                mv.visitVarInsn(ALOAD, 0);
                mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>",
                        "()V", false);
                // Create branch target
                Label target = new Label();
                mv.visitLabel(target);
                // System.out.println("Hello");
                mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out",
                        "Ljava/io/PrintStream;");
                mv.visitLdcInsn("Hello");
                mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream",
                        "println", "(Ljava/lang/String;)V", false);
                // switch(0) {
                mv.visitInsn(ICONST_0);
                // default: goto target;
                // }
                mv.visitLookupSwitchInsn(target, new int[0], new Label[0]);
                mv.visitMaxs(-1, -1);
                mv.visitEnd();
                cw.visitEnd();
                byte[] bytes = cw.toByteArray();
                return defineClass(name, bytes, 0, bytes.length);
            }
        }.loadClass("LookupGotoTest").newInstance();
    }
}

However if you replace V1_6 with V1_7 , it fails with the following error:

Exception in thread "main" java.lang.VerifyError: Bad instruction
Exception Details:
  Location:
    LookupGotoTest.<init>()V @13: lookupswitch
  Reason:
    Error exists in the bytecode
  Bytecode:
    0x0000000: 2ab7 0008 b200 0e12 10b6 0016 03ab 0000
    0x0000010: ffff fff7 0000 0000                    
  Stackmap Table:
    full_frame(@4,{Object[#2]},{})

    at java.lang.Class.getDeclaredConstructors0(Native Method)
    at java.lang.Class.privateGetDeclaredConstructors(Class.java:2658)
    at java.lang.Class.getConstructor0(Class.java:3062)
    at java.lang.Class.newInstance(Class.java:403)
    at LookupTest.main(LookupTest.java:46)

However if I make the forward jump instead and add a goto instruction, it works fine even with 1.7 bytecode:

Label target2 = new Label();
// switch(0) {
mv.visitInsn(ICONST_0);
// default: goto target2;
// }
mv.visitLookupSwitchInsn(target2, new int[0], new Label[0]);
mv.visitLabel(target2);
// goto target
mv.visitJumpInsn(GOTO, target);

The difference appears due to different verification procedure: Java classes prior to Java 1.6 has no StackMapTable and verified by Type Inference , while classes with version 1.7 or higher are verified by Type Checking which has separate strict rules for individual instructions including lookupswitch .

Currently it's unclear for me whether such instruction is actually not allowed in 1.7+ or ASM just generated incorrect StackMapTable.


As @Holger and @apangin noted, this is likely an ASM bug and can be worked around adding at least one case branch via mv.visitLookupSwitchInsn(target, new int[]{1}, new Label[]{target}); . So in conclusion: yes, you can generate backward branch in switch using any bytecode version.

Having say that, is there a possible way of looping infinitely by using only those opcodes by modifying a class or generating it with assembler?

To have an infinite loop you have to just backward somewhere. If you modify the byte code this can happen where ever you add or change a jump to go back. If not it can't be a loop, infinite or otherwise.

At the bytecode level, everything is gotos, basically. A tableswitch or lookupswitch instruction is just a list of offsets to jump to. You can make it jump backwards if you want. You can't make it jump directly to itself, but that's only because it pops an int off the stack each time. If you prefix it with an int push, you can have a 2 instruction loop.

Consider the following source code:

public static void main(String... arg) {
    loop: for(;;) switch(arg.length) {
        case 0: continue;
        default: break loop;
    }
}

When compiling it with Oracle's javac (jdk1.8), you'll get

public static void main(java.lang.String...)
  Code:
     0: aload_0
     1: arraylength
     2: lookupswitch  { // 1
                   0: 20
             default: 23
        }
    20: goto          0
    23: goto          26
    26: return

This obviously is a straight-forward translation, but this result is not mandatory. The goto s at the end are actually obsolete and by compiling with Eclipse 4.4.2, I got:

public static void main(java.lang.String...) t
  Code:
     0: aload_0
     1: arraylength
     2: tableswitch   { // 0 to 0
                   0: 20
             default: 23
        }
    20: goto          0
    23: return

So this compiler already omitted one of these obsolete goto s. But it's imaginable that another compiler would even eliminate the other goto as well, without changing the semantic:

public static void main(java.lang.String...) t
  Code:
     0: aload_0
     1: arraylength
     2: tableswitch   { // 0 to 0
                   0: 0
             default: 20
        }
    20: return

It's also imaginable that a byte code optimization tool is capable of taking either of the former results and transform it into the third variant. Since this is all without changing the semantic of the code, it's all still reflecting the valid Java source code shown above.

So having a switch bytecode instruction producing a loop doesn't necessarily represent a logic not reproducible in Java source code. It's just a compiler implementation dependent property when they never produce such a construct but more redundant code instead. Keep always in mind that both, while / for loops and switch statements, are source code artifacts and not mandating a particular byte code form.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM