Why does JNI's `FindClass` method have weird side effects?

Viewed 128

I have encountered a very strange issue with JNI. Can anybody please help me understand what is wrong here?

If I run the code below as-is, I see:

(a) 7fb6f022faf0 7fb6f022fb00 0
(b) 7fb6f022faf8 7fb6f022fb00 1

If I uncomment the line marked // (*), then I get:

(a) 7f6ce822faf0 7f6ce822fb08 1
(b) 7f6ce822fb00 7f6ce822fb08 1

With the (*) line commented out (which should be a no-op!), one instance of Integer.class is found to be unequal to another instance of Integer.class, using the Class.equals method. With the line uncommented, java.lang.Integer is looked up twice rather than once in the test1 method, and for some reason now the two instances of Integer.class are found to be equal! (This is on JDK 16.)

What the heck? I don't understand this at all...

pkg/Test.java:

package pkg;
public class Test {
    public static native void test0();
    public static native void test1(Object... args);

    public static void main(String[] args) throws Exception {
        test0();
        test1(7);
    }
}

test.c:

#include <jni.h>
#include <stdio.h>

jclass Integer_class_0;

JNIEXPORT void JNICALL Java_pkg_Test_test0(JNIEnv *env, jclass ignored) {
    Integer_class_0 = (*env)->FindClass(env, "java/lang/Integer");
}

JNIEXPORT void JNICALL Java_pkg_Test_test1(JNIEnv *env, jclass ignored, 
            jobjectArray args) {
    //(*env)->FindClass(env, "java/lang/Integer");    // (*)
    jobject arg = (*env)->GetObjectArrayElement(env, args, 0);
    jclass arg_type = (*env)->GetObjectClass(env, arg);
    jclass Integer_class_1 = (*env)->FindClass(env, "java/lang/Integer");

    jclass cls_class = (*env)->FindClass(env, "java/lang/Class");
    jmethodID cls_equals_methodID =
            (*env)->GetMethodID(env, cls_class, "equals", "(Ljava/lang/Object;)Z");

    printf("(a) %lx %lx %d\n", Integer_class_0, Integer_class_1,
            (*env)->CallBooleanMethod(env,
                    Integer_class_0, cls_equals_methodID, Integer_class_1));
    printf("(b) %lx %lx %d\n", arg_type, Integer_class_1,
            (*env)->CallBooleanMethod(env,
                    arg_type, cls_equals_methodID, Integer_class_1));
}

2 Answers

Mind the JNI Specification:

The JNI divides object references used by the native code into two categories: local and global references. Local references are valid for the duration of a native method call, and are automatically freed after the native method returns. Global references remain valid until they are explicitly freed.

Objects are passed to native methods as local references. All Java objects returned by JNI functions are local references. The JNI allows the programmer to create global references from local references. JNI functions that expect Java objects accept both global and local references. A native method may return a local or global reference to the VM as its result.

You get a local reference from (*env)->FindClass(env, "java/lang/Integer") within the test0 method call and try to use it within the test1 method call, despite it has been automatically freed when test0 returned.

Accessing freed memory or invalid references can have arbitrary effects, including the possibility that an apparently unrelated operation changes the outcome.

To get a reference that persists between method calls, you have to use NewGlobalRef.

By the way, you can use IsSameObject to compare the references, without calling equals (which would have different semantics when overridden).

This entire experiment is built on shaky foundations:

  1. Integer_class_0 is a local reference, which is made invalid when Java_pkg_Test_test0 returns. Attempting to use it in Java_pkg_Test_test1 makes no sense. Unless you make it a global reference first with NewGlobalRef!
  2. The JNI returns handles to object references, so it makes no sense to compare the returned handles directly. If you want to see if two handles point to the same object, use env->IsSameObject(handle1, handle2) or equals (although that checks more than just object identity).

The best explanation I can think of is that the "no-op" FindClass call stores a local reference at first slot of the local reference table (which is also where Integer_class_0 points), which accidentally makes Integer_class_0 reference the Integer class again. One way to validate this hypothesis would be:

env->FindClass("java/lang/String"); // replaces the local reference pointed to by Integer_class_0
jclass clsClass = env->FindClass("java/lang/Class");
jmethodID midClassToString = env->GetMethodID("toString", "()Ljava/lang/String;");
jstring className = (jstring) env->CallObjectMethod(Integer_class_0, midClassToString);

The resulting className object should then contain class java.lang.String instead of class java.lang.Integer.

Related