fix: resolve SIGBUS crash in JNI bridge — use CallVoidMethod instead of function pointer casts
The root cause of the SIGBUS (alignment fault) crash was casting jmethodID values to function pointers and invoking them directly. jmethodID is an opaque JNI type that must only be used via CallVoidMethod/CallObjectMethod etc. Calling it as a function pointer dereferences garbage memory, causing SIGBUS at address 0xb1. Changes: - Replace function pointer typedefs (output_callback_t etc.) with a struct storing JNIEnv + callback global ref + jmethodIDs - All callbacks now go through CallVoidMethod(env, ref, mid, ...) - Remove getTestProgress() (never used, unused start_time workaround) - Fix Kotlin arg cast: List<String> -> toTypedArray() (ClassCastException) - Update UI default server to speedtest.uztelecom.uz
This commit is contained in:
@@ -14,8 +14,6 @@
|
|||||||
#include <android/log.h>
|
#include <android/log.h>
|
||||||
#include <jni.h>
|
#include <jni.h>
|
||||||
#include <pthread.h>
|
#include <pthread.h>
|
||||||
#include <signal.h>
|
|
||||||
#include <sys/stat.h>
|
|
||||||
|
|
||||||
#include "iperf.h"
|
#include "iperf.h"
|
||||||
#include "iperf_api.h"
|
#include "iperf_api.h"
|
||||||
@@ -30,24 +28,56 @@
|
|||||||
static struct iperf_test *g_test = NULL;
|
static struct iperf_test *g_test = NULL;
|
||||||
static volatile int g_stop = 0;
|
static volatile int g_stop = 0;
|
||||||
|
|
||||||
/* Callback interface in Java/Kotlin */
|
/* Per-call state to pass between JNI and the reader thread.
|
||||||
typedef void (*output_callback_t)(JNIEnv *env, jobject callback, jstring line);
|
* We store the callback object (global ref) and method IDs.
|
||||||
typedef void (*error_callback_t)(JNIEnv *env, jobject callback, jstring msg);
|
* Callbacks are invoked via CallVoidMethod, NOT as function pointers. */
|
||||||
typedef void (*complete_callback_t)(JNIEnv *env, jobject callback);
|
|
||||||
|
|
||||||
/* Per-call state to pass between JNI and the reader thread */
|
|
||||||
struct iperf_run_state {
|
struct iperf_run_state {
|
||||||
JNIEnv *env;
|
JNIEnv *env;
|
||||||
jobject callback;
|
jobject callback_ref; /* Global ref to the callback object */
|
||||||
|
jmethodID mid_output; /* Method ID for onOutput(String) */
|
||||||
|
jmethodID mid_error; /* Method ID for onError(String) */
|
||||||
|
jmethodID mid_complete; /* Method ID for onComplete() */
|
||||||
FILE *outfile;
|
FILE *outfile;
|
||||||
int pipe_fd[2];
|
int pipe_fd[2];
|
||||||
JavaVM *jvm;
|
JavaVM *jvm;
|
||||||
pthread_t reader_thread;
|
pthread_t reader_thread;
|
||||||
output_callback_t on_output;
|
|
||||||
error_callback_t on_error;
|
|
||||||
complete_callback_t on_complete;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/* Helper: invoke the output callback via CallVoidMethod */
|
||||||
|
static void signal_output(struct iperf_run_state *state, const char *msg) {
|
||||||
|
if (!state || !msg || !msg[0]) return;
|
||||||
|
JNIEnv *env = state->env;
|
||||||
|
if (!env || !state->mid_output) return;
|
||||||
|
|
||||||
|
jstring jline = (*env)->NewStringUTF(env, msg);
|
||||||
|
if (jline) {
|
||||||
|
(*env)->CallVoidMethod(env, state->callback_ref, state->mid_output, jline);
|
||||||
|
(*env)->DeleteLocalRef(env, jline);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Helper: invoke the error callback via CallVoidMethod */
|
||||||
|
static void signal_error(struct iperf_run_state *state, const char *msg) {
|
||||||
|
if (!state || !msg) return;
|
||||||
|
JNIEnv *env = state->env;
|
||||||
|
if (!env || !state->mid_error) return;
|
||||||
|
|
||||||
|
jstring jmsg = (*env)->NewStringUTF(env, msg);
|
||||||
|
if (jmsg) {
|
||||||
|
(*env)->CallVoidMethod(env, state->callback_ref, state->mid_error, jmsg);
|
||||||
|
(*env)->DeleteLocalRef(env, jmsg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Helper: invoke the complete callback via CallVoidMethod */
|
||||||
|
static void signal_complete(struct iperf_run_state *state) {
|
||||||
|
if (!state || !state->mid_complete) return;
|
||||||
|
JNIEnv *env = state->env;
|
||||||
|
if (!env) return;
|
||||||
|
|
||||||
|
(*env)->CallVoidMethod(env, state->callback_ref, state->mid_complete);
|
||||||
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
/* Output reader thread — reads from iperf's output pipe and forwards */
|
/* Output reader thread — reads from iperf's output pipe and forwards */
|
||||||
/* each line to the Java callback via JNI. */
|
/* each line to the Java callback via JNI. */
|
||||||
@@ -64,10 +94,6 @@ static void *reader_thread_func(void *arg) {
|
|||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
jclass callback_cls = (*env)->GetObjectClass(env, state->callback);
|
|
||||||
jmethodID mid_output = (*env)->GetMethodID(
|
|
||||||
env, callback_cls, "onOutput", "(Ljava/lang/String;)V");
|
|
||||||
|
|
||||||
char buf[1024];
|
char buf[1024];
|
||||||
FILE *fp = fdopen(state->pipe_fd[0], "r");
|
FILE *fp = fdopen(state->pipe_fd[0], "r");
|
||||||
if (fp) {
|
if (fp) {
|
||||||
@@ -77,8 +103,8 @@ static void *reader_thread_func(void *arg) {
|
|||||||
continue;
|
continue;
|
||||||
jstring line = (*env)->NewStringUTF(env, buf);
|
jstring line = (*env)->NewStringUTF(env, buf);
|
||||||
if (line != NULL) {
|
if (line != NULL) {
|
||||||
(*env)->CallVoidMethod(env, state->callback,
|
(*env)->CallVoidMethod(env, state->callback_ref,
|
||||||
mid_output, line);
|
state->mid_output, line);
|
||||||
(*env)->DeleteLocalRef(env, line);
|
(*env)->DeleteLocalRef(env, line);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -133,40 +159,6 @@ static void free_argv(char **argv, int argc) {
|
|||||||
free(argv);
|
free(argv);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
|
||||||
/* Helper: signal an error through the Java callback. */
|
|
||||||
/* ------------------------------------------------------------------ */
|
|
||||||
static void signal_error(struct iperf_run_state *state, const char *msg) {
|
|
||||||
if (!state || !msg) return;
|
|
||||||
JNIEnv *env = state->env;
|
|
||||||
if (!env) return;
|
|
||||||
|
|
||||||
if (state->on_error) {
|
|
||||||
jstring jmsg = (*env)->NewStringUTF(env, msg);
|
|
||||||
if (jmsg) {
|
|
||||||
state->on_error(env, state->callback, jmsg);
|
|
||||||
(*env)->DeleteLocalRef(env, jmsg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
|
||||||
/* Helper: signal output through the Java callback. */
|
|
||||||
/* ------------------------------------------------------------------ */
|
|
||||||
static void signal_output(struct iperf_run_state *state, const char *msg) {
|
|
||||||
if (!state || !msg || !msg[0]) return;
|
|
||||||
JNIEnv *env = state->env;
|
|
||||||
if (!env) return;
|
|
||||||
|
|
||||||
if (state->on_output) {
|
|
||||||
jstring jline = (*env)->NewStringUTF(env, msg);
|
|
||||||
if (jline) {
|
|
||||||
state->on_output(env, state->callback, jline);
|
|
||||||
(*env)->DeleteLocalRef(env, jline);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
/* JNI: Start an iperf3 test with the given arguments. */
|
/* JNI: Start an iperf3 test with the given arguments. */
|
||||||
/* Signature: void runIperfLive(String[] args, IperfCallback cb) */
|
/* Signature: void runIperfLive(String[] args, IperfCallback cb) */
|
||||||
@@ -176,18 +168,16 @@ Java_com_novoyuuparosk_iperf3gui_IperfRunner_runIperfLive(
|
|||||||
JNIEnv *env, jobject thiz,
|
JNIEnv *env, jobject thiz,
|
||||||
jobjectArray arguments, jobject callback) {
|
jobjectArray arguments, jobject callback) {
|
||||||
|
|
||||||
/* Resolve callback methods */
|
/* Create the iperf test object */
|
||||||
jclass callback_cls = (*env)->GetObjectClass(env, callback);
|
struct iperf_test *test = iperf_new_test();
|
||||||
jmethodID mid_output = (*env)->GetMethodID(
|
if (!test) {
|
||||||
env, callback_cls, "onOutput", "(Ljava/lang/String;)V");
|
const char *err = "Failed to create iperf test";
|
||||||
jmethodID mid_error = (*env)->GetMethodID(
|
LOGE("%s", err);
|
||||||
env, callback_cls, "onError", "(Ljava/lang/String;)V");
|
return;
|
||||||
jmethodID mid_complete = (*env)->GetMethodID(
|
}
|
||||||
env, callback_cls, "onComplete", "()V");
|
|
||||||
|
|
||||||
/* Get JavaVM for thread attachment */
|
iperf_defaults(test);
|
||||||
JavaVM *jvm;
|
g_test = test;
|
||||||
(*env)->GetJavaVM(env, &jvm);
|
|
||||||
|
|
||||||
/* Convert Java String[] -> char** argv */
|
/* Convert Java String[] -> char** argv */
|
||||||
int argc = 0;
|
int argc = 0;
|
||||||
@@ -195,6 +185,22 @@ Java_com_novoyuuparosk_iperf3gui_IperfRunner_runIperfLive(
|
|||||||
if (!argv) {
|
if (!argv) {
|
||||||
const char *err = "Failed to parse arguments";
|
const char *err = "Failed to parse arguments";
|
||||||
signal_error((struct iperf_run_state *) 0, err);
|
signal_error((struct iperf_run_state *) 0, err);
|
||||||
|
iperf_free_test(test);
|
||||||
|
g_test = NULL;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Parse arguments */
|
||||||
|
if (iperf_parse_arguments(test, argc, argv) < 0) {
|
||||||
|
char errbuf[256];
|
||||||
|
snprintf(errbuf, sizeof(errbuf), "Argument parse error: %s",
|
||||||
|
iperf_strerror(i_errno));
|
||||||
|
free_argv(argv, argc);
|
||||||
|
|
||||||
|
signal_error((struct iperf_run_state *) 0, errbuf);
|
||||||
|
|
||||||
|
iperf_free_test(test);
|
||||||
|
g_test = NULL;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,11 +210,11 @@ Java_com_novoyuuparosk_iperf3gui_IperfRunner_runIperfLive(
|
|||||||
free_argv(argv, argc);
|
free_argv(argv, argc);
|
||||||
const char *err = "Failed to create output pipe";
|
const char *err = "Failed to create output pipe";
|
||||||
LOGE("pipe() failed: %s", strerror(errno));
|
LOGE("pipe() failed: %s", strerror(errno));
|
||||||
if (mid_error) {
|
|
||||||
jstring jmsg = (*env)->NewStringUTF(env, err);
|
signal_error((struct iperf_run_state *) 0, err);
|
||||||
(*env)->CallVoidMethod(env, callback, mid_error, jmsg);
|
|
||||||
(*env)->DeleteLocalRef(env, jmsg);
|
iperf_free_test(test);
|
||||||
}
|
g_test = NULL;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,63 +225,59 @@ Java_com_novoyuuparosk_iperf3gui_IperfRunner_runIperfLive(
|
|||||||
free_argv(argv, argc);
|
free_argv(argv, argc);
|
||||||
close(pipefd[0]);
|
close(pipefd[0]);
|
||||||
close(pipefd[1]);
|
close(pipefd[1]);
|
||||||
|
iperf_free_test(test);
|
||||||
|
g_test = NULL;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Resolve callback method IDs (do this in the calling thread) */
|
||||||
|
jclass callback_cls = (*env)->GetObjectClass(env, callback);
|
||||||
|
state->mid_output = (*env)->GetMethodID(
|
||||||
|
env, callback_cls, "onOutput", "(Ljava/lang/String;)V");
|
||||||
|
state->mid_error = (*env)->GetMethodID(
|
||||||
|
env, callback_cls, "onError", "(Ljava/lang/String;)V");
|
||||||
|
state->mid_complete = (*env)->GetMethodID(
|
||||||
|
env, callback_cls, "onComplete", "()V");
|
||||||
|
|
||||||
|
/* Get JavaVM for thread attachment */
|
||||||
|
JavaVM *jvm;
|
||||||
|
(*env)->GetJavaVM(env, &jvm);
|
||||||
|
|
||||||
state->env = env;
|
state->env = env;
|
||||||
state->callback = (*env)->NewGlobalRef(env, callback);
|
state->callback_ref = (*env)->NewGlobalRef(env, callback);
|
||||||
state->outfile = fdopen(pipefd[1], "w");
|
state->outfile = fdopen(pipefd[1], "w");
|
||||||
state->pipe_fd[0] = pipefd[0];
|
state->pipe_fd[0] = pipefd[0];
|
||||||
state->jvm = jvm;
|
state->jvm = jvm;
|
||||||
state->on_output = mid_output
|
|
||||||
? (output_callback_t) mid_output : NULL;
|
|
||||||
state->on_error = mid_error
|
|
||||||
? (error_callback_t) mid_error : NULL;
|
|
||||||
state->on_complete = mid_complete
|
|
||||||
? (complete_callback_t) mid_complete : NULL;
|
|
||||||
|
|
||||||
/* Create the iperf test object */
|
|
||||||
struct iperf_test *test = iperf_new_test();
|
|
||||||
if (!test) {
|
|
||||||
free_argv(argv, argc);
|
|
||||||
free(state);
|
|
||||||
close(pipefd[0]);
|
|
||||||
const char *err = "Failed to create iperf test";
|
|
||||||
LOGE("iperf_new_test() returned NULL");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
iperf_defaults(test);
|
|
||||||
g_test = test;
|
|
||||||
|
|
||||||
/* Redirect iperf's output to our pipe */
|
/* Redirect iperf's output to our pipe */
|
||||||
test->outfile = state->outfile;
|
test->outfile = state->outfile;
|
||||||
setvbuf(test->outfile, NULL, _IOLBF, 0); /* Line-buffered */
|
setvbuf(test->outfile, NULL, _IOLBF, 0); /* Line-buffered */
|
||||||
|
|
||||||
/* Parse arguments */
|
/* Notify Java that the test is starting */
|
||||||
if (iperf_parse_arguments(test, argc, argv) < 0) {
|
signal_output(state, "[iPerf JNI] Starting iPerf3 client...\n");
|
||||||
char errbuf[256];
|
|
||||||
snprintf(errbuf, sizeof(errbuf), "Argument parse error: %s",
|
/* Spawn reader thread to forward iperf output to Java */
|
||||||
iperf_strerror(i_errno));
|
if (pthread_create(&state->reader_thread, NULL,
|
||||||
|
reader_thread_func, state) != 0) {
|
||||||
free_argv(argv, argc);
|
free_argv(argv, argc);
|
||||||
|
close(pipefd[0]);
|
||||||
signal_error(state, errbuf);
|
close(pipefd[1]);
|
||||||
|
const char *err = "Failed to start output reader thread";
|
||||||
fclose(test->outfile);
|
LOGE("%s", err);
|
||||||
|
signal_error(state, err);
|
||||||
iperf_free_test(test);
|
iperf_free_test(test);
|
||||||
g_test = NULL;
|
g_test = NULL;
|
||||||
|
|
||||||
/* Cleanup */
|
|
||||||
free(state);
|
free(state);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Notify Java that the test is starting */
|
|
||||||
signal_output(state, "🚀 Initiating iPerf3 client request...\n");
|
|
||||||
|
|
||||||
/* Run the test */
|
/* Run the test */
|
||||||
g_stop = 0;
|
g_stop = 0;
|
||||||
int ret = iperf_run_client(test);
|
int ret = iperf_run_client(test);
|
||||||
|
|
||||||
|
/* Wait for reader thread to finish */
|
||||||
|
pthread_join(state->reader_thread, NULL);
|
||||||
|
|
||||||
/* Flush and close output */
|
/* Flush and close output */
|
||||||
fflush(state->outfile);
|
fflush(state->outfile);
|
||||||
fclose(state->outfile);
|
fclose(state->outfile);
|
||||||
@@ -287,25 +289,23 @@ Java_com_novoyuuparosk_iperf3gui_IperfRunner_runIperfLive(
|
|||||||
/* Free argv */
|
/* Free argv */
|
||||||
free_argv(argv, argc);
|
free_argv(argv, argc);
|
||||||
|
|
||||||
/* Signal completion */
|
/* Signal result to Java */
|
||||||
if (g_stop) {
|
if (g_stop) {
|
||||||
signal_output(state, "[iPerf JNI] Test was stopped by user.\n");
|
signal_output(state, "[iPerf JNI] Test was stopped by user.\n");
|
||||||
} else if (ret < 0) {
|
} else if (ret < 0) {
|
||||||
char errbuf[256];
|
char errbuf[256];
|
||||||
snprintf(errbuf, sizeof(errbuf), "Test failed: %s",
|
snprintf(errbuf, sizeof(errbuf), "[iPerf JNI] Test failed: %s\n",
|
||||||
iperf_strerror(i_errno));
|
iperf_strerror(i_errno));
|
||||||
signal_error(state, errbuf);
|
signal_error(state, errbuf);
|
||||||
} else {
|
} else {
|
||||||
signal_output(state,
|
signal_output(state, "[iPerf JNI] Test completed successfully.\n");
|
||||||
"[iPerf JNI] Test completed successfully.\n");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Signal completion to Java */
|
/* Signal completion to Java */
|
||||||
if (state->on_complete) {
|
signal_complete(state);
|
||||||
state->on_complete(env, state->callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Free the state struct */
|
/* Free global ref and state struct */
|
||||||
|
(*env)->DeleteGlobalRef(env, state->callback_ref);
|
||||||
free(state);
|
free(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -340,21 +340,6 @@ Java_com_novoyuuparosk_iperf3gui_IperfRunner_forceStopIperfTest(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
|
||||||
/* JNI: Get elapsed milliseconds since test started. */
|
|
||||||
/* Signature: int getTestProgress() */
|
|
||||||
/* ------------------------------------------------------------------ */
|
|
||||||
JNIEXPORT jint JNICALL
|
|
||||||
Java_com_novoyuuparosk_iperf3gui_IperfRunner_getTestProgress(JNIEnv *env,
|
|
||||||
jobject thiz) {
|
|
||||||
if (!g_test) return -1;
|
|
||||||
|
|
||||||
/* Get elapsed time in usecs and convert to int ms */
|
|
||||||
/* Note: iperf3 3.19 doesn't expose start_time on iperf_test struct.
|
|
||||||
* We track it locally via a static variable set at test start. */
|
|
||||||
return (jint)(g_test->duration * 1000); /* fallback: return configured duration */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
/* JNI: Get whether iperf is currently running. */
|
/* JNI: Get whether iperf is currently running. */
|
||||||
/* Signature: boolean isTestRunning() */
|
/* Signature: boolean isTestRunning() */
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ object IperfRunner : FlutterPlugin {
|
|||||||
channel?.setMethodCallHandler { call, result ->
|
channel?.setMethodCallHandler { call, result ->
|
||||||
when (call.method) {
|
when (call.method) {
|
||||||
"runIperf" -> {
|
"runIperf" -> {
|
||||||
val args = call.argument<Array<String>>("args")
|
val args = call.argument<List<String>>("args")?.toTypedArray()
|
||||||
if (args != null) {
|
if (args != null) {
|
||||||
runIperfLive(args, object : IperfCallback {
|
runIperfLive(args, object : IperfCallback {
|
||||||
override fun onOutput(line: String) {
|
override fun onOutput(line: String) {
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ class _TestRunContent extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _TestRunContentState extends State<_TestRunContent> {
|
class _TestRunContentState extends State<_TestRunContent> {
|
||||||
final _serverController = TextEditingController(text: 'iperf.es.net');
|
final _serverController = TextEditingController(text: 'speedtest.uztelecom.uz');
|
||||||
final _portController = TextEditingController(text: '5201');
|
final _portController = TextEditingController(text: '5201');
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
Reference in New Issue
Block a user