Saturday, April 23, 2016

java.lang.VerifyError IllformedLocaleException

Leave a Comment

I have the following parent method, that is used in all cases by various API levels:

public int setVoice (@NonNull final String language, @NonNull final String region){     if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {         return setVoice21(language, region);     } else {         return setVoiceDeprecated(language, region);     } } 

and setVoice21 does something like this:

@TargetApi(Build.VERSION_CODES.LOLLIPOP) public int setVoice21 ( @NonNull final String language, @NonNull final String region){      try {         // try some API 21 stuff     } catch (final IllformedLocaleException e) {         e.printStackTrace();         return setVoiceDeprecated(language, region);     } 

setVoice21 contains other code that requires API 21+ specifically TextToSpeech.Voice and Locale.Builder

When I run this code on a device < API 21 I'm getting the following error:

W/dalvikvm: VFY: unable to resolve exception class 6232 (Ljava/util/IllformedLocaleException;) W/dalvikvm: VFY: rejecting opcode 0x0d at 0x0168 W/dalvikvm: VFY: rejected Lcom/myapp/android/speech/MyTextToSpeech;.setVoice21 (Ljava/lang/String;Ljava/lang/String;)I W/dalvikvm: Verifier rejected class Lcom/myapp/android/speech/MyTextToSpeech;

E/AndroidRuntime: FATAL EXCEPTION: main java.lang.VerifyError: com/myapp/android/speech/MyTextToSpeech

If I remove the IllformedLocaleException and just replace it with a standard Exception, the app runs fine, despite the many other references to methods > API21 within setVoice21

To confuse me yet further, setVoice21 invokes the following class

@TargetApi(Build.VERSION_CODES.LOLLIPOP) private class TTSVoice {      public void buildVoice() {          try {             // Do some API 21 stuff         } catch (final IllformedLocaleException e) {         }      } } 

This class is only referenced from setVoice21, but I do not have to remove the reference to IllformedLocaleException here - I can leave it and the app runs fine.... Baffled.

Can anyone help me out as to why the IllformedLocaleException is causing this failure? Are Exceptions somehow handled differently?

I thank you in advance.

Note - I'm not sure that it is relevant, but I'm subclassing TextToSpeech in a standard way. I fear this may convolute the question, but just in case...

public class MyTextToSpeech extends TextToSpeech {      public MyTextToSpeech(final Context context, final OnInitListener listener) {         super(context, listener);     } } 

EDIT - The workaround provided by razzledazzle below, does allow the app to run without crashing, but I still remain non-the-wiser as to why such a step is necessary. I've never had to take such measures before when dealing with API versioning.

2 Answers

Answers 1

Resolved the issue by removing the IllformedLocaleException class from the catch argument. This will still allow you to check for IllformedLocaleException.

@TargetApi(Build.VERSION_CODES.LOLLIPOP) public int setVoice21 (@NonNull final String language, @NonNull final String region) {     try {         // try some API 21 stuff         ...     } catch (final Exception e) {         e.printStackTrace();         if (e instanceof IllformedLocaleException) {             ...         }     }      ... } 

Answers 2

TL;DR: Exceptions are exceptional. Can't catch an exception whose type is not known.

The following is most speculation based on my limited knowledge about Java/Dalvik and common sense. Take it with a grain of salt. I found the method that spits out the failing log line and confirmed most of my mentioned speculations, see added links below.

Your problem seems to be that the classes are loaded at once, either the whole class is loaded or none of it. Verification is done first I guess to prevent some runtime checks (remember Android is resource constrained).

I used the following code:

@TargetApi(Build.VERSION_CODES.LOLLIPOP) public int setVoice21(@NonNull final String language, @NonNull final String region) {     try {         // try some API 21 stuff         new Locale.Builder().build().getDisplayVariant();     } catch (final IllformedLocaleException ex) {         ex.printStackTrace();     }     return 0; } 

When the system was trying to create an instance of the class containing this method the following happened:

E/dalvikvm: Could not find class 'java.util.Locale$Builder', referenced from method com.test.TestFragment.setVoice21

Loading the Locale.Builder class would be a ClassNotFoundException.

W/dalvikvm: VFY: unable to resolve new-instance 5241 (Ljava/util/Locale$Builder;) in Lcom/test/TestFragment;
D/dalvikvm: VFY: replacing opcode 0x22 at 0x0000

Then on that non-existent class it would try to call the <init> method which is prevented by replacing the OP_NEW_INSTANCE with a OP_NOP. I think this would have been survivable, as I see these all the time when using the support library. I think the assumption here is that if the class is not found then it must have been guarded with an SDK_INT check. Also if it went through dexing/proguard and other stuff it must've been intentional and a ClassNotFoundException is acceptable at runtime.

W/dalvikvm: VFY: unable to resolve exception class 5234 (Ljava/util/IllformedLocaleException;)

Another problematic class, notice this time it's an "exception class" which must be special. If you check the Java bytecode for this method via:

javap -verbose -l -private -c -s TestFragment.class > TestFragment.dis  public int setVoice21(java.lang.String, java.lang.String);     ...     Exception table:        from    to  target type            0    14    17   Class java/util/IllformedLocaleException     LocalVariableTable:       Start  Length  Slot  Name   Signature          18      11     3    ex   Ljava/util/IllformedLocaleException;           0      31     0  this   Lcom/test/TestFragment;           0      31     1 language   Ljava/lang/String;           0      31     2 region   Ljava/lang/String;     StackMapTable: number_of_entries = 2       frame_type = 81 /* same_locals_1_stack_item */         stack = [ class java/util/IllformedLocaleException ]       frame_type = 11 /* same */ 

You can indeed see that the Exception table and StackMapTable and LocalVariableTable all contain the problematic class, but not Locale$Builder. This may be because the builder is not stored in a variable, but the point to take from here is that exceptions are handled specially and get more scrutiny than normal lines of code.

Using BakSmali on the APK via:

apktool.bat d -r -f -o .\disassembled "app-debug.apk"  .method public setVoice21(Ljava/lang/String;Ljava/lang/String;)I .prologue :try_start_0 new-instance v1, Ljava/util/Locale$Builder; invoke-direct {v1}, Ljava/util/Locale$Builder;-><init>()V ... :try_end_0 .catch Ljava/util/IllformedLocaleException; {:try_start_0 .. :try_end_0} :catch_0 ... :catch_0 move-exception v0 .local v0, "ex":Ljava/util/IllformedLocaleException; invoke-virtual {v0}, Ljava/util/IllformedLocaleException;->printStackTrace()V 

seems to reveal a similar pattern, here we can actually see the op-codes mentioned in the log. Notice that .catch seems to be a special instruction, not an operation because it's preceded by a dot. I think this reinforces the scrutiny mentioned above: it's not a runtime operation, but it is required for the class to load the code contained within the methods.

W/dalvikvm: VFY: unable to find exception handler at addr 0xe
W/dalvikvm: VFY: rejected Lcom/test/TestFragment;.setVoice21 (Ljava/lang/String;Ljava/lang/String;)I

I guess this means that it was not able to reconstruct when to call which catch block from the Exception table and StackMapTable because it couldn't find the class to determine the parent classes. This is confirmed in getCaughtExceptionType where "unable to resolve exception class" directly leads to "unable to find exception handler" because it finds no common super-class for a non-existent exception, something like } catch (? ex) { so it doesn't know what to catch.

W/dalvikvm: VFY: rejecting opcode 0x0d at 0x000e
W/dalvikvm: VFY: rejected Lcom/test/TestFragment;.setVoice21 (Ljava/lang/String;Ljava/lang/String;)I

I think at this point the verifier just gave up because it couldn't make sense of the OP_MOVE_EXCEPTION. This is confirmed as that the getCaughtExceptionType method is only used in one place, a switch. Breaking out of that we get "rejecting opcode" then it goto bails up the call stack to "rejected class". After bailing the error code was VERIFY_ERROR_GENERIC which is mapped to VerifyError. Couldn't find where the actual JNI Exception is thrown if it even works that way.

W/dalvikvm: Verifier rejected class Lcom/test/TestFragment;

Multiple rejections were filed against the setVoice21 method and hence the whole class must be rejected (this seems harsh to me, it's possible ART is different in this regard).

W/dalvikvm: Class init failed in newInstance call (Lcom/test/TestFragment;)
D/AndroidRuntime: Shutting down VM
W/dalvikvm: threadid=1: thread exiting with uncaught exception (group=0x41869da0)
E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.bumptech.glide.supportapp.v3, PID: 27649
java.lang.VerifyError: com/test/TestFragment

I guess this is similar to an ExceptionInInitializerError in desktop Java which is thrown when a static { } in class body or an static field initializer throws a RuntimeException/Error.

Why instanceof works

Using razzledazzle's workaround changes those tables to include java/lang/Exception, and moves the dependency to IllformedLocaleException into the code to be executed at runtime:

     0    14    17   Class java/lang/Exception 19: instanceof    #34                 // class java/util/IllformedLocaleException 

and similarly the Smali:

.catch Ljava/lang/Exception; {:try_start_0 .. :try_end_0} :catch_0 instance-of v1, v0, Ljava/util/IllformedLocaleException; 

E/dalvikvm: Could not find class 'java.util.IllformedLocaleException', referenced from method com.test.TestFragment.setVoice21
W/dalvikvm: VFY: unable to resolve instanceof 5234 (Ljava/util/IllformedLocaleException;) in Lcom/test/TestFragment;

Now, it's the same complaint as for Locale$Builder above

D/dalvikvm: VFY: replacing opcode 0x20 at 0x000f

Replacing OP_INSTANCE_OF with ?something?, it doesn't say :)

Another possible workaround

If you look at android.support.v4.view.ViewCompat* classes you will notice that not all those classes are used on all versions. The correct one is chosen at runtime (search for static final ViewCompatImpl IMPL in ViewCompat.java) and only that is loaded. This ensures that even at class load time there won't be any weirdness due to missing classes and is performant. You can do a similar architecture to prevent that method from loading on earlier API levels.

If You Enjoyed This, Take 5 Seconds To Share It

0 comments:

Post a Comment