fix: resolve argv[0] misparse and SIGSEGV hang in JNI bridge
Two root causes prevented iperf3 tests from running: 1. argv[0] bug: iperf_parse_arguments() expects argv[0] to be the program name. We passed Dart args directly, so argv[0] was '-c', which iperf3 misinterpreted as the program name → 'Argument parse error: must either be a client (-c) or server (-s)'. Fixed by prepending "iperf3" as argv[0] and shifting user args to argv[1+]. 2. Signal handling: iperf_catch_sigend() installs handlers that use longjmp() to a jmp_buf, but we never initialized the buffer with setjmp(). Any signal during iperf_run_client() caused SIGSEGV or hang. Added setjmp/longjmp wrapper matching main.c's pattern, SIGPIPE=SIG_IGN, and a 30s connect_timeout to avoid DNS hangs. Also extracted iperf_cleanup_and_signal() to deduplicate the normal-return and longjmp paths. Added diagnostic logging to IperfRunner.kt's library loading for faster future debugging.
This commit is contained in:
@@ -4,6 +4,13 @@
|
|||||||
* This file exposes iperf3's API to Java/Kotlin via JNI, enabling
|
* This file exposes iperf3's API to Java/Kotlin via JNI, enabling
|
||||||
* the Flutter app to run iperf3 tests natively inside the app process
|
* the Flutter app to run iperf3 tests natively inside the app process
|
||||||
* (no Process.start needed, no SELinux restrictions).
|
* (no Process.start needed, no SELinux restrictions).
|
||||||
|
*
|
||||||
|
* SIGNAL HANDLING NOTE:
|
||||||
|
* iperf3's run() function (called from iperf_run_client()) uses
|
||||||
|
* setjmp/longjmp for graceful signal handling. The JNI bridge must
|
||||||
|
* replicate this setup — otherwise iperf_catch_sigend() installs
|
||||||
|
* handlers that longjmp to an unset buffer, causing undefined
|
||||||
|
* behaviour (typically a SIGSEGV or hang).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
@@ -11,6 +18,8 @@
|
|||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
|
#include <setjmp.h>
|
||||||
|
#include <signal.h>
|
||||||
#include <android/log.h>
|
#include <android/log.h>
|
||||||
#include <jni.h>
|
#include <jni.h>
|
||||||
#include <pthread.h>
|
#include <pthread.h>
|
||||||
@@ -159,6 +168,62 @@ static void free_argv(char **argv, int argc) {
|
|||||||
free(argv);
|
free(argv);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* Internal: clean up after iperf_run_client returns (normal path). */
|
||||||
|
/* Must be called only on the normal return path (NOT after longjmp). */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
static void iperf_cleanup_and_signal(JNIEnv *env,
|
||||||
|
struct iperf_run_state *state,
|
||||||
|
char **argv, int argc,
|
||||||
|
int ret, int stopped) {
|
||||||
|
/* Wait for reader thread to finish */
|
||||||
|
pthread_join(state->reader_thread, NULL);
|
||||||
|
|
||||||
|
/* Flush and close output */
|
||||||
|
fflush(state->outfile);
|
||||||
|
fclose(state->outfile);
|
||||||
|
|
||||||
|
/* Clean up iperf */
|
||||||
|
iperf_free_test(g_test);
|
||||||
|
g_test = NULL;
|
||||||
|
|
||||||
|
/* Free argv */
|
||||||
|
free_argv(argv, argc);
|
||||||
|
|
||||||
|
/* Signal result to Java */
|
||||||
|
if (stopped) {
|
||||||
|
signal_output(state, "[iPerf JNI] Test was stopped by user.\n");
|
||||||
|
} else if (ret < 0) {
|
||||||
|
char errbuf[256];
|
||||||
|
snprintf(errbuf, sizeof(errbuf),
|
||||||
|
"[iPerf JNI] Test failed: %s\n",
|
||||||
|
iperf_strerror(i_errno));
|
||||||
|
signal_error(state, errbuf);
|
||||||
|
} else {
|
||||||
|
signal_output(state, "[iPerf JNI] Test completed successfully.\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Signal completion to Java */
|
||||||
|
signal_complete(state);
|
||||||
|
|
||||||
|
/* Free global ref and state struct */
|
||||||
|
(*env)->DeleteGlobalRef(env, state->callback_ref);
|
||||||
|
free(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* Per-call signal handling state. */
|
||||||
|
/* Must be static so it survives across the longjmp back to setjmp. */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
static jmp_buf g_sigend_jmp_buf;
|
||||||
|
static volatile int g_signed_sig = 0;
|
||||||
|
|
||||||
|
/* Signal handler — mirrors main.c::sigend_handler */
|
||||||
|
static void sigend_handler(int sig) {
|
||||||
|
g_signed_sig = sig;
|
||||||
|
longjmp(g_sigend_jmp_buf, 1);
|
||||||
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
/* 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) */
|
||||||
@@ -179,26 +244,47 @@ Java_com_novoyuuparosk_iperf3gui_IperfRunner_runIperfLive(
|
|||||||
iperf_defaults(test);
|
iperf_defaults(test);
|
||||||
g_test = test;
|
g_test = test;
|
||||||
|
|
||||||
/* Convert Java String[] -> char** argv */
|
/*
|
||||||
int argc = 0;
|
* Convert Java String[] -> char** argv, prepending "iperf3" as argv[0].
|
||||||
char **argv = java_string_array_to_argv(env, arguments, &argc);
|
* iperf_parse_arguments() expects argv[0] to be the program name;
|
||||||
|
* passing args directly from Dart means argv[0] would be "-c",
|
||||||
|
* which iperf3 misinterprets as the program name, not the client flag.
|
||||||
|
*/
|
||||||
|
int dart_argc = (*env)->GetArrayLength(env, arguments);
|
||||||
|
int total_argc = dart_argc + 1; /* +1 for "iperf3" */
|
||||||
|
char **argv = malloc((total_argc + 1) * sizeof(char *));
|
||||||
if (!argv) {
|
if (!argv) {
|
||||||
const char *err = "Failed to parse arguments";
|
const char *err = "Failed to allocate argv";
|
||||||
signal_error((struct iperf_run_state *) 0, err);
|
LOGE("%s", err);
|
||||||
iperf_free_test(test);
|
iperf_free_test(test);
|
||||||
g_test = NULL;
|
g_test = NULL;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
argv[0] = strdup("iperf3");
|
||||||
|
for (int i = 0; i < dart_argc; i++) {
|
||||||
|
jstring jstr = (jstring) (*env)->GetObjectArrayElement(env, arguments, i);
|
||||||
|
const char *cstr = (*env)->GetStringUTFChars(env, jstr, NULL);
|
||||||
|
if (cstr) {
|
||||||
|
argv[i + 1] = strdup(cstr);
|
||||||
|
(*env)->ReleaseStringUTFChars(env, jstr, cstr);
|
||||||
|
} else {
|
||||||
|
argv[i + 1] = strdup("");
|
||||||
|
}
|
||||||
|
(*env)->DeleteLocalRef(env, jstr);
|
||||||
|
}
|
||||||
|
argv[total_argc] = NULL; /* NULL-terminated for iperf */
|
||||||
|
|
||||||
|
/* Set a reasonable connect timeout (30s) to avoid hanging on DNS resolution.
|
||||||
|
* Default iperf3 connect timeout is 120s which is far too long. */
|
||||||
|
test->settings->connect_timeout = 30000;
|
||||||
|
|
||||||
/* Parse arguments */
|
/* Parse arguments */
|
||||||
if (iperf_parse_arguments(test, argc, argv) < 0) {
|
if (iperf_parse_arguments(test, total_argc, argv) < 0) {
|
||||||
char errbuf[256];
|
char errbuf[256];
|
||||||
snprintf(errbuf, sizeof(errbuf), "Argument parse error: %s",
|
snprintf(errbuf, sizeof(errbuf),
|
||||||
iperf_strerror(i_errno));
|
"Argument parse error: %s", iperf_strerror(i_errno));
|
||||||
free_argv(argv, argc);
|
LOGE("%s", errbuf);
|
||||||
|
free_argv(argv, total_argc);
|
||||||
signal_error((struct iperf_run_state *) 0, errbuf);
|
|
||||||
|
|
||||||
iperf_free_test(test);
|
iperf_free_test(test);
|
||||||
g_test = NULL;
|
g_test = NULL;
|
||||||
return;
|
return;
|
||||||
@@ -207,7 +293,7 @@ Java_com_novoyuuparosk_iperf3gui_IperfRunner_runIperfLive(
|
|||||||
/* Create a pipe to capture iperf's stdout/stderr */
|
/* Create a pipe to capture iperf's stdout/stderr */
|
||||||
int pipefd[2];
|
int pipefd[2];
|
||||||
if (pipe(pipefd) != 0) {
|
if (pipe(pipefd) != 0) {
|
||||||
free_argv(argv, argc);
|
free_argv(argv, total_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));
|
||||||
|
|
||||||
@@ -222,7 +308,7 @@ Java_com_novoyuuparosk_iperf3gui_IperfRunner_runIperfLive(
|
|||||||
struct iperf_run_state *state =
|
struct iperf_run_state *state =
|
||||||
calloc(1, sizeof(struct iperf_run_state));
|
calloc(1, sizeof(struct iperf_run_state));
|
||||||
if (!state) {
|
if (!state) {
|
||||||
free_argv(argv, argc);
|
free_argv(argv, total_argc);
|
||||||
close(pipefd[0]);
|
close(pipefd[0]);
|
||||||
close(pipefd[1]);
|
close(pipefd[1]);
|
||||||
iperf_free_test(test);
|
iperf_free_test(test);
|
||||||
@@ -253,60 +339,72 @@ Java_com_novoyuuparosk_iperf3gui_IperfRunner_runIperfLive(
|
|||||||
test->outfile = state->outfile;
|
test->outfile = state->outfile;
|
||||||
setvbuf(test->outfile, NULL, _IOLBF, 0); /* Line-buffered */
|
setvbuf(test->outfile, NULL, _IOLBF, 0); /* Line-buffered */
|
||||||
|
|
||||||
/* Notify Java that the test is starting */
|
/*
|
||||||
signal_output(state, "[iPerf JNI] Starting iPerf3 client...\n");
|
* Set up signal handling BEFORE calling iperf_run_client().
|
||||||
|
* iperf3's run() function uses setjmp/longjmp for signal handling.
|
||||||
|
* - setjmp returns 0 on first call (normal path)
|
||||||
|
* - setjmp returns non-zero when called via longjmp (signal path)
|
||||||
|
*
|
||||||
|
* Both paths share the same `state`, `argv`, `argc` variables,
|
||||||
|
* so the stack frame is preserved across the longjmp.
|
||||||
|
*/
|
||||||
|
if (setjmp(g_sigend_jmp_buf) == 0) {
|
||||||
|
/* Normal path — run the test */
|
||||||
|
signal(SIGPIPE, SIG_IGN); /* Ignore SIGPIPE for socket errors */
|
||||||
|
iperf_catch_sigend(sigend_handler);
|
||||||
|
|
||||||
/* Spawn reader thread to forward iperf output to Java */
|
/* Notify Java that the test is starting */
|
||||||
if (pthread_create(&state->reader_thread, NULL,
|
signal_output(state, "[iPerf JNI] Starting iPerf3 client...\n");
|
||||||
reader_thread_func, state) != 0) {
|
|
||||||
free_argv(argv, argc);
|
|
||||||
close(pipefd[0]);
|
|
||||||
close(pipefd[1]);
|
|
||||||
const char *err = "Failed to start output reader thread";
|
|
||||||
LOGE("%s", err);
|
|
||||||
signal_error(state, err);
|
|
||||||
iperf_free_test(test);
|
|
||||||
g_test = NULL;
|
|
||||||
free(state);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Run the test */
|
/* Spawn reader thread to forward iperf output to Java */
|
||||||
g_stop = 0;
|
if (pthread_create(&state->reader_thread, NULL,
|
||||||
int ret = iperf_run_client(test);
|
reader_thread_func, state) != 0) {
|
||||||
|
free_argv(argv, total_argc);
|
||||||
|
close(pipefd[0]);
|
||||||
|
close(pipefd[1]);
|
||||||
|
const char *err = "Failed to start output reader thread";
|
||||||
|
LOGE("%s", err);
|
||||||
|
signal_error(state, err);
|
||||||
|
iperf_free_test(test);
|
||||||
|
g_test = NULL;
|
||||||
|
free(state);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
/* Wait for reader thread to finish */
|
/* Run the test */
|
||||||
pthread_join(state->reader_thread, NULL);
|
g_stop = 0;
|
||||||
|
int ret = iperf_run_client(test);
|
||||||
|
|
||||||
/* Flush and close output */
|
/*
|
||||||
fflush(state->outfile);
|
* We get here after iperf_run_client returns normally.
|
||||||
fclose(state->outfile);
|
* If a signal arrives during iperf_run_client(), the
|
||||||
|
* sigend_handler fires longjmp() and we never reach here.
|
||||||
/* Clean up iperf */
|
*/
|
||||||
iperf_free_test(test);
|
iperf_cleanup_and_signal(env, state, argv, total_argc, ret, 0);
|
||||||
g_test = NULL;
|
|
||||||
|
|
||||||
/* Free argv */
|
|
||||||
free_argv(argv, argc);
|
|
||||||
|
|
||||||
/* Signal result to Java */
|
|
||||||
if (g_stop) {
|
|
||||||
signal_output(state, "[iPerf JNI] Test was stopped by user.\n");
|
|
||||||
} else if (ret < 0) {
|
|
||||||
char errbuf[256];
|
|
||||||
snprintf(errbuf, sizeof(errbuf), "[iPerf JNI] Test failed: %s\n",
|
|
||||||
iperf_strerror(i_errno));
|
|
||||||
signal_error(state, errbuf);
|
|
||||||
} else {
|
} else {
|
||||||
signal_output(state, "[iPerf JNI] Test completed successfully.\n");
|
/*
|
||||||
|
* We got here via longjmp() from sigend_handler.
|
||||||
|
* Clean up resources allocated before the setjmp.
|
||||||
|
* NOTE: state, argv, argc are still valid on the stack.
|
||||||
|
*/
|
||||||
|
LOGI("Test interrupted by signal %d", g_signed_sig);
|
||||||
|
|
||||||
|
/* Stop flag for cleanup */
|
||||||
|
g_stop = 1;
|
||||||
|
|
||||||
|
/* Close the output file (the pipe write end) */
|
||||||
|
if (state->outfile) {
|
||||||
|
fclose(state->outfile);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Free argv (argv is still valid across longjmp since it's on stack) */
|
||||||
|
free_argv(argv, total_argc);
|
||||||
|
|
||||||
|
/* Run the cleanup with stopped=true.
|
||||||
|
* Note: argv is NULL, argc=0 so free_argv is a no-op.
|
||||||
|
* The state struct will be freed inside this function. */
|
||||||
|
iperf_cleanup_and_signal(env, state, NULL, 0, 0, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Signal completion to Java */
|
|
||||||
signal_complete(state);
|
|
||||||
|
|
||||||
/* Free global ref and state struct */
|
|
||||||
(*env)->DeleteGlobalRef(env, state->callback_ref);
|
|
||||||
free(state);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
|
|||||||
@@ -19,7 +19,14 @@ object IperfRunner : FlutterPlugin {
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
// Load native library at class initialization
|
// Load native library at class initialization
|
||||||
System.loadLibrary("iperf3jni")
|
try {
|
||||||
|
android.util.Log.i("IperfRunner", "Loading native library...")
|
||||||
|
System.loadLibrary("iperf3jni")
|
||||||
|
android.util.Log.i("IperfRunner", "Native library loaded successfully")
|
||||||
|
} catch (e: UnsatisfiedLinkError) {
|
||||||
|
android.util.Log.e("IperfRunner", "Failed to load native library: ${e.message}")
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||||
|
|||||||
Reference in New Issue
Block a user