java.­lang.­reflect.­TypeVariable getBounds is not thread safe

March 20, 2017

java.lang.reflect.TypeVariable getBounds is not thread safe. Calling it from multiple threads might even crash your JVM

The following method shows you the use of getBounds(). The method getBounds is used to get the upper bound(s) of a generic type:

public void testGetBounds() { Class cl = GenericInterface.class; TypeVariable typeVariable  = cl.getTypeParameters()[0]; typeVariable.getBounds()[0].getTypeName(); }

To make the examples and tests shorter, I do not iterate over the returned array but simply use the first element. Here is the generic interface used in the example:

package com.vmlens.stressTest.util; public interface GenericInterface> { }

If you call testGetBounds from multiple threads, calling getBounds leads to a race condition.

The race condition

Since the array of TypeVariables returned by getTypeParameters is cached in the volatile field genericInfo, each thread works on the same TypeVariable instance. And the class TypeVariableImpl implementing the TypeVariable interface modifies the not volatile field bounds without synchronization:
package sun.reflect.generics.reflectiveObjects;
// import statements omitted
public class TypeVariableImpl
    extends LazyReflectiveObjectGenerator implements TypeVariable {
    // upper bounds - evaluated lazily
    private Type[] bounds;
    public Type[] getBounds() {
        // lazily initialize bounds if necessary
        if (bounds == null) {
            FieldTypeSignature[] fts = getBoundASTs(); // get AST
            // allocate result array; note that
            // keeping ts and bounds separate helps with threads
            Type[] ts = new Type[fts.length];
            // iterate over bound trees, reifying each in turn
            for ( int j = 0; j  < fts.length; j++) {
                Reifier r = getReifier();
                fts[j].accept(r);
                ts[j] = r.getResult();
            }
            // cache result
            bounds = ts;
            // could throw away bound ASTs here; thread safety?
        }
        return bounds.clone(); // return cached bounds
    }
    // other fields and methods omitted
}

In line 15 and 30 the field bounds is read and in line 27 it is written. If the code is executed in the given order everything is o.k. But if some component reorders the statements, the array is not completely initialized. In pseudo code the method getBounds looks like this:

if instance variable bounds is  null { set local variable ts to new Array initialize the array set instance variable bounds to the local variable ts } return instance variable bounds.clone

If the statements get reordered another thread sees an uninitialised array:

Thread A set local variable ts to new Array
Thread A set instance variable bounds to the local variable ts
Thread B if instance variable bounds is null
Thread B Thread B return instance variable bounds.clone // the array is not yet completely initialized

One such component is the cache system of the CPU. ARM compatible processors like in smartphones or the Raspberry Pi reorder reads and writes to improve performance, leading to a scenario as described above.

Reproducing the error

To reproduce the error I use jcstress, an open JDK code tool: The Java Concurrency Stress tests (jcstress) is an experimental harness and a suite of tests to aid the research in the correctness of concurrency support in the JVM, class libraries, and hardware.

I use the following test class:

@JCStressTest @Outcome(id = "0, 0", expect = Expect.ACCEPTABLE, desc = "Default outcome.") @State public class TypeVariableGetBounds { private final Class cl; public TypeVariableGetBounds() { try { cl = (new StressTestClassLoader(TypeVariableGetBounds.class.getClassLoader())) .loadClass("com.vmlens.stressTest.util.GenericInterface"); } catch (Exception e) { throw new RuntimeException("Test setup incorrect", e); } } public void callContainsDataRace() { TypeVariable typeVariable = cl.getTypeParameters()[0]; typeVariable.getBounds()[0].getTypeName(); } @Actor public void actor1(IntResult2 r) { callContainsDataRace(); } @Actor public void actor2(IntResult2 r) { callContainsDataRace(); } }

Jcstress runs this test multiple times always calling the method actor1 and actor2 from separate threads. To separate the tests, I use a special classloader, which always reloads a class.

When I call this test on a raspberry pi using the test mode tough or stress, I see a crash of the JVM:

A fatal error has been detected by the Java Runtime Environment:

SIGSEGV (0xb) at pc=0x7665b4c0, pid=16112, tid=1680598112

JRE version: Java(TM) SE Runtime Environment (8.0_65-b17) (build 1.8.0_65-b17)

Java VM: Java HotSpot(TM) Client VM (25.65-b01 mixed mode linux-arm )

Problematic frame:

V [libjvm.so+0x27a4c0]

The JVM error log shows the following:
Stack: [0x6426f000,0x642bf000],  sp=0x642bd8b8,  free space=314k
Native frames: (J=compiled Java code, j=interpreted, Vv=VM code, C=native code)
V  [libjvm.so+0x27a4c0]
Java frames: (J=compiled Java code, j=interpreted, Vv=VM code)
J 1303  java.lang.Object.clone()Ljava/lang/Object; (0 bytes) @ 0x742ba71c [0x742ba6e0+0x3c]
J 1313 C1 sun.reflect.generics.repository.GenericDeclRepository.getTypeParameters()[Ljava/lang/reflect/TypeVariable; (80 bytes) @ 0x742b8210 [0x742b7f40+0x2d0]
J 1229 C1 com.vmlens.stressTest.tests.TypeVariableGetBounds_jcstress.actor1()Ljava/lang/Void; (109 bytes) @ 0x742a6cb8 [0x742a6bf0+0xc8]
j  com.vmlens.stressTest.tests.TypeVariableGetBounds_jcstress$$Lambda$6.call()Ljava/lang/Object;+4
j  java.util.concurrent.FutureTask.run()V+42
j  java.util.concurrent.ThreadPoolExecutor.runWorker(Ljava/util/concurrent/ThreadPoolExecutor$Worker;)V+95
j  java.util.concurrent.ThreadPoolExecutor$Worker.run()V+5
j  java.lang.Thread.run()V+11
v  ~StubRoutines::call_stub

It seems that the native array clone method can not cope with an uninitialized array.

So far I could not reproduce this error on my intel i5 workstation.

Conclusion

java.lang.reflect.TypeVariable getBounds is not thread safe. Calling it from multiple threads might lead, depending on the java platform you are using, to strange errors.

Make your application thread safe

LEARN MORE