Repository: jpype-project/jpype (language: python)

Issue:
JVM crash on Windows when loading a class with (partially) unsatisfied dependencies
On Windows, when loading a class through JPype which has partially unsatisfied dependencies, e.g. in return types, the JVM crashes.

When loading the same class through the "java" binary, as well as with JPype on Linux, I get a NoClassDefFoundError as expected.

This situation can occur when Java projects are compiled with compile-time only dependencies (e.g. for unit tests) but classes using these dependencies are still included in the final JARs. The classes concerned should not be loadable (NoClassDefFoundError), but should not crash the JVM either.

Minimal example
```java
public class TestClass {
    public static void main(String[] args) {
        System.out.println("Hi Freaks");
    }

    public static UnsatisfiedClass methodWithUnsatisfiedReturnType() {
        return null;
    }
}
```
```java
public class UnsatisfiedClass {
}
```

To build:
```
javac TestClass.java
rm UnsatisfiedClass.class
```

Running with "java" on Windows:
```
D:\jptest>java TestClass
Error: Unable to initialize main class TestClass
Caused by: java.lang.NoClassDefFoundError: UnsatisfiedClass
```

JPype 1.2.1 on Windows:
```
(base) D:\jptest>python
Python 3.7.3 (default, Apr 24 2019, 15:29:51) [MSC v.1915 64 bit (AMD64)] :: Anaconda, Inc. on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import jpype
>>> jpype.__version__
'1.2.1'
>>> jpype.getDefaultJVMPath()
'C:\\Program Files\\Java\\jdk-11.0.8\\bin\\server\\jvm.dll'
>>> jpype.startJVM(classpath='.')
>>> jpype.JClass('TestClass')
#
# A fatal error has been detected by the Java Runtime Environment:
#
#  EXCEPTION_UNCAUGHT_CXX_EXCEPTION (0xe06d7363) at pc=0x00007ff9a3684048, pid=7912, tid=6524
#
# JRE version: Java(TM) SE Runtime Environment 18.9 (11.0.8+10) (build 11.0.8+10-LTS)
# Java VM: Java HotSpot(TM) 64-Bit Server VM 18.9 (11.0.8+10-LTS, mixed mode, tiered, compressed oops, g1 gc, windows-amd64)
# Problematic frame:
# C  [KERNELBASE.dll+0x14048]
#
# No core dump will be written. Minidumps are not enabled by default on client versions of Windows
#
# An error report file with more information is saved as:
# D:\jptest\hs_err_pid7912.log
#
# If you would like to submit a bug report, please visit:
#   https://bugreport.java.com/bugreport/crash.jsp
# The crash happened outside the Java Virtual Machine in native code.
# See problematic frame for where to report the bug.
#
```
Full crash dump: [hs_err_pid7912.log](https://github.com/jpype-project/jpype/files/5855419/hs_err_pid7912.log)


JPype 1.2.1 on Linux (not exactly the same environment through):
```
(venv) [mihostet@... pyt]$ python
Python 3.6.5 (default, Jun 15 2019, 23:43:46) 
[GCC 7.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import jpype
>>> jpype.startJVM(classpath='.')
>>> jpype.JClass('TestClass')
Traceback (most recent call last):
  File "ClassLoader.java", line 522, in java.lang.ClassLoader.loadClass
  File "ClassLoaders.java", line 178, in jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass
  File "BuiltinClassLoader.java", line 581, in jdk.internal.loader.BuiltinClassLoader.loadClass
java.lang.ClassNotFoundException: java.lang.ClassNotFoundException: UnsatisfiedClass

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "org.jpype.JPypeContext.java", line -1, in org.jpype.JPypeContext.newWrapper
  File "org.jpype.manager.TypeFactoryNative.java", line -2, in org.jpype.manager.TypeFactoryNative.newWrapper
  File "org.jpype.manager.TypeManager.java", line -1, in org.jpype.manager.TypeManager.populateMembers
  File "org.jpype.manager.TypeManager.java", line -1, in org.jpype.manager.TypeManager.createMembers
  File "org.jpype.manager.TypeManager.java", line -1, in org.jpype.manager.TypeManager.createMethodDispatches
  File "Class.java", line 1904, in java.lang.Class.getMethods
  File "Class.java", line 3191, in java.lang.Class.privateGetPublicMethods
  File "Class.java", line 3166, in java.lang.Class.privateGetDeclaredMethods
  File "Class.java", line -2, in java.lang.Class.getDeclaredMethods0
Exception: Java Exception

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/afs/cern.ch/user/m/mihostet/tmp/pyt/venv/lib/python3.6/site-packages/jpype/_jclass.py", line 99, in __new__
    return _jpype._getClass(jc)
java.lang.NoClassDefFoundError: java.lang.NoClassDefFoundError: UnsatisfiedClass
```



Discussion / hints:
Thanks.  I will try to replicate this.
The call that is leading to a crash is "cls.getMethods()".   There is nothing different between windows and linux on the code path.  When called from Linux the java call properly throws, but when called from windows it generates two exceptions and dies.

```
Internal exceptions (2 events):                                                                                               Event: 0.135 Thread 0x0000021df3dc3000 Exception <a 'sun/nio/fs/WindowsException'{0x00000007134d3808}> (0x00000007134d3808) thrown at [t:/workspace/open/src/hotspot/share/prims/jni.cpp, line 615]                                                         Event: 0.374 Thread 0x0000021df3dc3000 Exception <a 'java/lang/NoClassDefFoundError'{0x00000007042519d0}: UnsatisfiedClass> (0x00000007042519d0) thrown at [t:/workspace/open/src/hotspot/share/classfile/systemDictionary.cpp, line 220]                    
```

The same series of calls executed from Java appear to work properly.   I added a try catch at it doesn't look like control is being returned to JPype.   However, if I intentional create an exception at that point it also leads to a crash, so clearly we are in a fragile state at that point.   Most likely we have a partially completed class that is triggering problems either immediately or on clean up of the bad state.

The cleanest solution would be to discover the problem when we have a cleaner stack before a bad resource gets created.  Adding a call to getMethods() and getFields() seems to resolve the issue.


Fix patch:
diff --git a/doc/CHANGELOG.rst b/doc/CHANGELOG.rst
index 61a130c75..dd4748533 100644
--- a/doc/CHANGELOG.rst
+++ b/doc/CHANGELOG.rst
@@ -5,6 +5,9 @@ This changelog *only* contains changes from the *first* pypi release (0.5.4.3) o
 
 Latest Changes:
 - **1.2.2_dev0 - 2021-01-03**
+
+  - Fixed issue with classes with unsatified dependencies leading to a crash
+    on windows.
   
   - Fixed a bug with arrays created using the short cut.  The wrong type
     was being returned.
diff --git a/native/java/org/jpype/manager/TypeManager.java b/native/java/org/jpype/manager/TypeManager.java
index 87f4c88db..a36f8d7de 100644
--- a/native/java/org/jpype/manager/TypeManager.java
+++ b/native/java/org/jpype/manager/TypeManager.java
@@ -180,7 +180,7 @@ public long findClassByName(String name)
   public Class<?> lookupByName(String name)
   {
     ClassLoader classLoader = JPypeContext.getInstance().getClassLoader();
-    
+
     // Handle arrays
     if (name.endsWith("[]"))
     {
@@ -397,6 +397,12 @@ private ClassDescriptor createClass(Class<?> cls, boolean special)
 
   private ClassDescriptor createOrdinaryClass(Class<?> cls, boolean special, boolean bases)
   {
+    // Verify the class will be loadable prior to creating the class.
+    // If we fail to do this then the class may end up crashing later when the
+    // members get populated which could leave us in a bad state.
+    cls.getMethods();
+    cls.getFields();
+
     // Object classes are more work as we need the super information as well.
     // Make sure all base classes are loaded
     Class<?> superClass = cls.getSuperclass();
diff --git a/project/jars/unsatisfied/build.sh b/project/jars/unsatisfied/build.sh
new file mode 100644
index 000000000..53e2f6a7f
--- /dev/null
+++ b/project/jars/unsatisfied/build.sh
@@ -0,0 +1,3 @@
+javac --release 7 -d classes src/org/jpype/unsatisfied/*.java
+rm classes/org/jpype/unsatisfied/UnsatisfiedClass.class
+jar --create --file unsatisfied.jar -C classes . 
diff --git a/project/jars/unsatisfied/src/org/jpype/unsatisfied/TestClass.java b/project/jars/unsatisfied/src/org/jpype/unsatisfied/TestClass.java
new file mode 100644
index 000000000..4e731c108
--- /dev/null
+++ b/project/jars/unsatisfied/src/org/jpype/unsatisfied/TestClass.java
@@ -0,0 +1,19 @@
+package org.jpype.unsatisfied;
+
+public class TestClass
+{
+
+  public static void main(String[] args)
+  {
+    System.out.println("Hi Freaks");
+  }
+
+  public static UnsatisfiedClass methodWithUnsatisfiedReturnType()
+  {
+    return null;
+  }
+}
+
+class UnsatisfiedClass
+{
+}


Test patch:
diff --git a/test/jar/unsatisfied.jar b/test/jar/unsatisfied.jar
new file mode 100644
index 000000000..04dbd7ad5
Binary files /dev/null and b/test/jar/unsatisfied.jar differ
diff --git a/test/jpypetest/test_jclass.py b/test/jpypetest/test_jclass.py
index 4911ae5a7..54d7db452 100644
--- a/test/jpypetest/test_jclass.py
+++ b/test/jpypetest/test_jclass.py
@@ -272,3 +272,9 @@ def testCanCast(self):
         String = JClass("java.lang.String")
         self.assertFalse(String._canCast(1))
         self.assertTrue(String._canCast("foo"))
+
+    def testUnsatisfied(self):
+        # This is testing what happens if a class fails to load properly due to 
+        # failures.  A partial loaded class can lead to crashes.
+        with self.assertRaises(JClass("java.lang.NoClassDefFoundError")):
+            JClass("org.jpype.unsatisfied.TestClass")