In this sample I will show how to avoid killing your JVM by the external library that makes a call to exit function. We will use atexit function to set a handler for exit calls, and then we will jump out of it using long jump.
After you complete this tutorial, you should end up with the layout similar to this one:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
recipeNo016 ├── Makefile ├── c │ ├── externalLib.c │ ├── recipeNo016_SigTerm.c │ ├── recipeNo016_SigTerm.h │ └── recipeNo016_SigTermNoHandler.h ├── java │ └── recipeNo016 │ ├── SigTerm.java │ └── SigTermNoHandler.java ├── lib │ ├── libExternalLib.dylib │ ├── libExternalLib.dylib.dSYM │ │ └── Contents │ │ ├── Info.plist │ │ └── Resources │ │ └── DWARF │ │ └── libExternalLib.dylib │ ├── libSigTerm.dylib │ └── libSigTerm.dylib.dSYM │ └── Contents │ ├── Info.plist │ └── Resources │ └── DWARF │ └── libSigTerm.dylib └── target └── recipeNo016 ├── SigTerm.class └── SigTermNoHandler.class |
In this tutorial we are using two Java classes that call methods from a single library – libSigTerm. SigTerm.java calls riskyCode() and wraps it with atexit based handler (check man atexit). SigTermNoHandler.java, on the other hand, makes the call to riskyCode() but doesn’t install atexit based handler.
First, let’s take a look at SigTerm.java code. You can find it below. Pay attention to atexit call (we are setting stopExit() to be the code that will be called whenever someone calls exit() – make sure to check man exit -S 3. Call to riskyCode() is wrapped by callExitCode().
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
/* SigTerm.java */ package recipeNo016; public class SigTerm { /* This is the native method we want to call */ public static native void callExitCode(); /* Inside static block we will load shared library */ static { System.loadLibrary("SigTerm"); } public static void main(String[] args) { /* This message will help you determine whether LD_LIBRARY_PATH is correctly set */ System.out.println("library: " + System.getProperty("java.library.path")); /* Call to shared library */ try { // We are surrounding with try - catch. // JNI will throw exception when exit function is called. System.out.println("Calling riskyCode() with atexit based handler"); SigTerm.callExitCode(); } catch(Exception ex) { // We will end up here catching exception produced by JNI. System.out.println("Exception - application tried to call exit"); } } } |
Second code, makes the same thing, but it calls different wrapper in JNI. This time, we call callExitCodeNoHandler(). In this case, native code will call riskyCode() without any precautions. You have to take a look at the JNI code to get the feeling what’s the difference.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
/* SigTermNoHandler.java */ package recipeNo016; public class SigTermNoHandler { /* This is the native method we want to call */ public static native void callExitCodeNoHandler(); /* Inside static block we will load shared library */ static { System.loadLibrary("SigTerm"); } public static void main(String[] args) { /* This message will help you determine whether LD_LIBRARY_PATH is correctly set */ System.out.println("library: " + System.getProperty("java.library.path")); /* Call to shared library */ try { // We are surrounding with try - catch, but it is hopeles. // JNI will not throw exception this time. System.out.println("Calling riskyCode() without atexit based handler"); SigTermNoHandler.callExitCodeNoHandler(); } catch(Exception ex) { // We will never reach this code. // Instead, JVM will finish with exit(1) code System.out.println("Exception - application tried to call exit"); } } } |
When it comes to JNI code generated by javac it’s plain simple.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
/* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class recipeNo016_SigTerm */ #ifndef _Included_recipeNo016_SigTerm #define _Included_recipeNo016_SigTerm #ifdef __cplusplus extern "C" { #endif /* * Class: recipeNo016_SigTerm * Method: callExitCode * Signature: ()V */ JNIEXPORT void JNICALL Java_recipeNo016_SigTerm_callExitCode (JNIEnv *, jclass); #ifdef __cplusplus } #endif #endif |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
/* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class recipeNo016_SigTermNoHandler */ #ifndef _Included_recipeNo016_SigTermNoHandler #define _Included_recipeNo016_SigTermNoHandler #ifdef __cplusplus extern "C" { #endif /* * Class: recipeNo016_SigTermNoHandler * Method: callExitCodeNoHandler * Signature: ()V */ JNIEXPORT void JNICALL Java_recipeNo016_SigTermNoHandler_callExitCodeNoHandler (JNIEnv *, jclass); #ifdef __cplusplus } #endif #endif |
As you can see – no surprises here. We just declare two, external functions: Java_recipeNo016_SigTermNoHandler_callExitCodeNoHandler and Java_recipeNo016_SigTerm_callExitCode.
Implementation is much more interesting. Take a look at different implementation of the call to riskyCode(). Compare Java_recipeNo016_SigTerm_callExitCode and Java_recipeNo016_SigTermNoHandler_callExitCodeNoHandler.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
#include <stdio.h> #include <signal.h> #include <setjmp.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include "jni.h" #include "recipeNo016_SigTerm.h" #include "recipeNo016_SigTermNoHandler.h" jmp_buf ljenv; // declaration of function for the lib void riskyCode(); // this is the handler for the risky code // if we reach here, it means somebody // tried to call exit void stopExit() { siglongjmp(ljenv, 1); } // riskCode protected by handler // this method will not exit from JVM // instead, it will throw exception JNIEXPORT void JNICALL Java_recipeNo016_SigTerm_callExitCode (JNIEnv *env, jclass obj) { atexit(stopExit); // set the long jump for the exit call handler // if handler is called it will jump // here with the error code specified // as parameter of siglongjmp // first call to sigsetjmp returns 0 if( sigsetjmp(ljenv,1) == 0) { // call the code that will try to exit riskyCode(); } else { // if the code tries to call exit, we are handling it // we can allocate a little bit more than we require char exceptionBuffer[1024]; sprintf(exceptionBuffer, "Error"); (*env)->ThrowNew( env, (*env)->FindClass( env, "java/lang/Exception"), exceptionBuffer); } } // unprotected call to riskyCode; JVM will exit after this call JNIEXPORT void JNICALL Java_recipeNo016_SigTermNoHandler_callExitCodeNoHandler (JNIEnv *env, jclass obj) { // we are calling risky code, but we don't set exit handler riskyCode(); } |
Can you see declaration of riskyCode()? This is the function from our nasty library that we use in our Java code. This function is defined inside externalLib.c, and later on, compiled into shared library.
1 2 3 4 5 6 7 8 9 10 |
#include <stdlib.h> #include <stdio.h> // the purpose of this code is to call exit function void riskyCode() { printf("Calling exit from C code\n"); exit(1); } |
When everything is in place, we can build all the stuff and perform the test
1 2 3 4 5 6 |
/Library/Java/JavaVirtualMachines/jdk1.8.0.jdk/Contents/Home/bin/javac -h c -d target java/recipeNo016/SigTerm.java /Library/Java/JavaVirtualMachines/jdk1.8.0.jdk/Contents/Home/bin/javac -h c -d target java/recipeNo016/SigTermNoHandler.java cc -g -shared -fpic c/externalLib.c -o lib/libExternalLib.dylib cc -g -shared -fpic -I/Library/Java/JavaVirtualMachines/jdk1.8.0.jdk/Contents/Home/include -I/Library/Java/JavaVirtualMachines/jdk1.8.0.jdk/Contents/Home/include/darwin -Llib -lExternalLib c/recipeNo016_SigTerm.c -o lib/libSigTerm.dylib |
When we start the test, we can see that we get two different outcomes of the execution
1 2 3 4 5 6 7 8 9 10 11 12 |
/Library/Java/JavaVirtualMachines/jdk1.8.0.jdk/Contents/Home/bin/java -Djava.library.path=:./lib -cp target recipeNo016.SigTerm library: :./lib Calling riskyCode() with atexit based handler Calling exit from C code Exception - application tried to call exit /Library/Java/JavaVirtualMachines/jdk1.8.0.jdk/Contents/Home/bin/java -Djava.library.path=:./lib -cp target recipeNo016.SigTermNoHandler library: :./lib Calling riskyCode() without atexit based handler Calling exit from C code make: *** [test] Error 1 |
As you can see, first call (wrapped with atexit) thrown an exception that was handled in JVM. Second call, without atexit function, made whole JVM to exit.