4

I have a minimal Android app created with the new project wizard with c++ support enabled. The purpose of the app is to allow c++ to call back into java after catching a signal (SIGSEGV). The sequence of the program is short and sweet, pseudo code would be like:

  1. Enter Native Method handleSegv()
    1. Native code calls back into java as a test
    2. Native code sets up SIGSEGV handler
  2. Enter Native Method sendSegv()
    1. Native code raises/sends SIGSEGV
  3. Enter Native Method signal_handler
    1. Native code catches signal and logs it
    2. Native code calls back into java
    3. Native code logs again to show its stepped past the callback

The only step above that isn't working is step 3.2. It seems that after catching SIGSEGV nothing happens when the native code tries to call back into java. I've tried this both in the emulator and on the device with the same results. I'm not sure at this point if I'm doing something wrong or if there is something fundamental about handling the signal that won't allow me to call back into java after catching it.

I have code demonstrating this which can be cloned from the repo from on github but really there are only two source files:

CrashActivity.java:

package com.kevinkreiser.crashtest;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;

public class CrashActivity extends AppCompatActivity {

    static {
        System.loadLibrary("native-lib");
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_crash);

        //setup segv handler
        handleSegv();
        //cause a segv
        sendSegv();
    }

    /**
     * Sets up signal handler for SIGSEGV which will call the callback function below
     * @return true if the handler was set
     */
    public native boolean handleSegv();

    /**
     * Raises the SIGSEGV signal which will cause the handler to be called
     */
    public native void sendSegv();

    /**
     * A function that the native code will call back when it receives SIGSEGV
     * as an illustration it just logs
     *
     * @param message  The message coming back from c++
     */
    public void callback(String message) {
        Log.e("CrashActivity.callback", message);
    }
}

native-lib.cpp:

#include <android/log.h>
#include <jni.h>
#include <string.h>
#include <signal.h>
#include <string>

//globals persisting between calls from javaland
static JavaVM* vm = NULL;
static jobject activity = NULL;
static jmethodID callback = NULL;

//gets called first when a signal is sent to the running pid
static void signal_handler(int signal, siginfo_t*, void*) {
    //get an env so we can call back to java
    JNIEnv* env;
    if(vm->AttachCurrentThread(&env, NULL) != JNI_OK)
        return;

    //call back to java with a message
    __android_log_print(ANDROID_LOG_ERROR, "native-lib.signal_handler", "Calling with signal %d", signal);
    std::string message = "Got signal " + std::to_string(signal);
    jstring msg = env->NewStringUTF(message.c_str());
    env->CallVoidMethod(activity, callback, msg);
    __android_log_print(ANDROID_LOG_ERROR, "native-lib.signal_handler", "Called with signal %d", signal);
}

extern "C" JNIEXPORT void JNICALL
Java_com_kevinkreiser_crashtest_CrashActivity_sendSegv(JNIEnv*, jobject) {
    raise(SIGSEGV);
}

extern "C" JNIEXPORT jboolean JNICALL
Java_com_kevinkreiser_crashtest_CrashActivity_handleSegv(JNIEnv* env, jobject obj) {
    //get java hooks we need to make the callback
    env->GetJavaVM(&vm);
    activity = env->NewGlobalRef(obj);
    if (!activity)
        return false;
    jclass activity_class = env->GetObjectClass(activity);
    if (!activity_class)
        return false;
    callback = env->GetMethodID(activity_class, "callback", "(Ljava/lang/String;)V");
    if (!callback)
        return false;

    //try calling back to java with a message
    jstring message = env->NewStringUTF("No signal yet");
    env->CallVoidMethod(activity, callback, message);

    //register for SIGSEGV
    struct sigaction action;
    memset(&action, 0, sizeof(struct sigaction));
    action.sa_sigaction = signal_handler;
    action.sa_flags = SA_SIGINFO;
    sigaction(SIGSEGV, &action, NULL);

    return true;
}

When I run the program and I look at logcats output I see the following:

2019-01-15 11:59:50.795 11183-11183/com.kevinkreiser.crashtest E/CrashActivity.callback: No signal yet
2019-01-15 11:59:50.795 11183-11183/com.kevinkreiser.crashtest E/native-lib.signal_handler: Calling with signal 11
2019-01-15 11:59:50.795 11183-11183/com.kevinkreiser.crashtest E/native-lib.signal_handler: Called with signal 11

If I step through the program with the debugger and set a breakpoint in the native signal_handler I can step down to the line where it logs the first time Calling with signal.... After this if I step over any line that includes a call using the JNIEnv (env in this case) the debugger will detatch and the program will finish. You'll note that from the logcat output though, that I do get the last native log line Called with signal... after the calls making use of env and most importantly the call back into java.

I've seen other implementations here on stackoverflow that do essentially this but I've not been able to get any of them to work. I've also tried throwing a java exception from the native code but that ends up also not getting back to javaland with a message about pending exceptions. Can anyone see what is wrong here? Thanks in advance!

Kevin Kreiser
  • 594
  • 5
  • 11
  • See e.g. https://stackoverflow.com/questions/16625447/how-to-call-a-java-function-via-jni-from-a-signal-handler-function-in-android and https://stackoverflow.com/questions/34547199/art-prevents-any-java-calls-from-jni-during-native-signal-handling – Michael Jan 15 '19 at 20:39
  • 1
    Yes, you're doing something wrong: you're invoking undefined behavior by calling non-async-signal-safe functions from within a signal handler. Absent specific documentation supporting calling a function, such as the POSIX list of async-safe functions, you really can't make *any* calls from a signal handler. [Footnote 188 of the C standard](https://port70.net/~nsz/c/c11/n1570.html#note188) even states: "Thus, a signal handler cannot, in general, call standard library functions." POSIX provides a list of functions that are safe to call - under POSIX. Anything else is undefined behavior. – Andrew Henle Jan 15 '19 at 21:33
  • @Michael I did indeed already see your first link, my code is essentially equivalent. The second link speaks more to my question about what is fundamentally possible vs not. The second seems to say that, yes, calling back into java after catching a signal isn't going to work. I'll do a bit more research about what can be done here. Thanks. – Kevin Kreiser Jan 15 '19 at 21:41

1 Answers1

1

@Andrew Henle's comment was the correct answer:

Yes, you're doing something wrong: you're invoking undefined behavior by calling non-async-signal-safe functions from within a signal handler. Absent specific documentation supporting calling a function, such as the POSIX list of async-safe functions, you really can't make any calls from a signal handler. Footnote 188 of the C standard even states: "Thus, a signal handler cannot, in general, call standard library functions." POSIX provides a list of functions that are safe to call - under POSIX. Anything else is undefined behavior.

He previously gave a more detailed answer to this question here: https://stackoverflow.com/a/34553070/5251867

EDIT:

Looking over the available functions it seems there are two avenues that one can persue.

  1. make use of open, write, close to drop a file with relevant info about the signal that was caught and deal with this file later (on app restart or from another service that monitors this file for changes)
  2. make use of connect, bind, send to send the details over a socket to some other process

I guess both of these are technically IPC since both are means of letting another process get access to the information the signal handler puts out there. Getting this info to another process where you can do something with the information seems to be the only suitable way to go forward.

Kevin Kreiser
  • 594
  • 5
  • 11