Reproduce Android JNI log callback failures on the host

This commit is contained in:
evgeny committed 2026-10-28 12:28:28 +01:00
1 parent 8268c5112c
commit c656e3a96b
3 files changed
+275

No files matched your search

@@ -0,0 +1,10 @@
/* Host-модель только вывода Android; JNI-типы и ABI берутся из установленного JDK. */
#ifndef UTUN_TEST_ANDROID_LOG_H
#define UTUN_TEST_ANDROID_LOG_H
#define ANDROID_LOG_VERBOSE 2
#define ANDROID_LOG_DEBUG 3
#define ANDROID_LOG_INFO 4
#define ANDROID_LOG_WARN 5
#define ANDROID_LOG_ERROR 6
int __android_log_print(int priority, const char* tag, const char* format, ...);
#endif
@@ -0,0 +1,38 @@
#!/usr/bin/env python3
"""Linux host: собирает настоящий Android bridge с JNI из JDK и запускает fault injection без телефона."""
import argparse
import os
from pathlib import Path
import shutil
import subprocess
import tempfile
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--java-home", type=Path)
args = parser.parse_args()
directory = Path(__file__).resolve().parent
root = directory.parents[3]
javac = shutil.which("javac")
java_home = args.java_home or (Path(javac).resolve().parents[1] if javac else None)
if java_home is None or not (java_home / "include/jni.h").is_file():
parser.error("JDK JNI headers missing; pass --java-home")
compiler = os.environ.get("CC", "cc")
includes = [directory, java_home / "include", java_home / "include/linux",
root / "src", root / "src/transport_layer", root / "src/routing_layer",
root / "src/chat", root / "src/media_async", root / "lib"]
with tempfile.TemporaryDirectory(prefix="utun-jni-log-") as output:
binary = Path(output) / "test_jni_log"
command = [compiler, "-std=c99", "-D_DEFAULT_SOURCE", "-D__ANDROID__", "-DUSE_OPENSSL",
"-ffunction-sections", "-fdata-sections", "-Wl,--gc-sections", "-pthread",
# Android JNI принимает JNIEnv**, JDK — void**; ABI одинаков, чужие callbacks здесь не вызываются.
"-Wno-incompatible-pointer-types"]
command += [f"-I{path}" for path in includes]
command += [str(directory / "test_jni_log.c"), "-o", str(binary)]
subprocess.run(command, check=True)
subprocess.run([str(binary)], check=True, timeout=15)
if __name__ == "__main__":
main()
@@ -0,0 +1,227 @@
/* Компилируем настоящий JNI-код, включая static callback, без тестовых веток в production.
* Неиспользуемые функции bridge отсекает ELF linker; JNI-модель ниже проверяет порядок вызовов и владение ссылками. */
#include "../../jni_bridge/android_jni_bridge.c"
enum fault { OK, GET_ENV, ATTACH, LOCAL_REF, OBJECT_CLASS, METHOD, CATEGORY, MESSAGE, COPY_CATEGORY, COPY_MESSAGE,
CALLBACK, DETACH, RECURSION, REPLACE };
struct ref { int live, kind; jsize length; unsigned char bytes[256]; };
static struct ref refs[16], global_ref;
static enum fault fault;
static int pending, throw_on_failure, illegal_calls, calls, attaches, detaches, descriptions, diagnostics, nested;
static int already_attached, arrays, copies, legacy_strings, payload_ok;
static char last_diagnostic[512], method_signature[128];
static JNIEnv test_env;
static const char category[] = "чат", message[] = "Ошибка 😀";
#define CHECK(test) do { if (!(test)) { fprintf(stderr, "[FAIL] %s:%d: %s\n", __func__, __LINE__, #test); return 1; } } while (0)
int __android_log_print(int priority, const char* tag, const char* format, ...) {
(void)tag;
if (priority != ANDROID_LOG_ERROR && priority != ANDROID_LOG_WARN) illegal_calls++;
va_list ap; va_start(ap, format);
vsnprintf(last_diagnostic, sizeof(last_diagnostic), format, ap);
va_end(ap); diagnostics++;
return 0;
}
static void check_pending(void) { if (pending) illegal_calls++; }
static void fail(void) { if (throw_on_failure) pending = 1; }
static jobject new_ref(int kind) {
for (unsigned i = 0; i < sizeof(refs) / sizeof(refs[0]); i++) {
if (!refs[i].live) { memset(&refs[i], 0, sizeof(refs[i])); refs[i].live = 1; refs[i].kind = kind; return (jobject)&refs[i]; }
}
illegal_calls++; return NULL;
}
static int live_refs(void) {
int count = 0;
for (unsigned i = 0; i < sizeof(refs) / sizeof(refs[0]); i++) count += refs[i].live;
return count;
}
static jobject JNICALL local_ref(JNIEnv* env, jobject object) {
(void)env; check_pending();
if (fault == LOCAL_REF) { fail(); return NULL; }
if (!object || !((struct ref*)object)->live) { illegal_calls++; return NULL; }
return new_ref(3);
}
static void JNICALL delete_ref(JNIEnv* env, jobject object) {
(void)env;
if (!object || !((struct ref*)object)->live) { illegal_calls++; return; }
((struct ref*)object)->live = 0;
}
static jclass JNICALL object_class(JNIEnv* env, jobject object) {
(void)env; check_pending();
if (!object || !((struct ref*)object)->live) illegal_calls++;
if (fault == OBJECT_CLASS) { fail(); return NULL; }
if (fault == REPLACE) { global_ref.live = 0; g_log_callback = NULL; }
return (jclass)new_ref(1);
}
static jmethodID JNICALL method(JNIEnv* env, jclass cls, const char* name, const char* signature) {
(void)env; check_pending();
if (!cls || !((struct ref*)cls)->live || strcmp(name, "onNativeLog")) illegal_calls++;
snprintf(method_signature, sizeof(method_signature), "%s", signature);
if (fault == METHOD || !cls) { fail(); return NULL; }
return (jmethodID)&method_signature;
}
static jbyteArray JNICALL byte_array(JNIEnv* env, jsize length) {
(void)env; check_pending(); arrays++;
if ((fault == CATEGORY && arrays == 1) || (fault == MESSAGE && arrays == 2)) { fail(); return NULL; }
if (length < 0 || length > 256) { illegal_calls++; return NULL; }
struct ref* ref = (struct ref*)new_ref(2);
if (ref) ref->length = length;
return (jbyteArray)ref;
}
static void JNICALL copy_bytes(JNIEnv* env, jbyteArray array, jsize start, jsize length, const jbyte* bytes) {
(void)env; check_pending(); copies++;
if ((fault == COPY_CATEGORY && copies == 1) || (fault == COPY_MESSAGE && copies == 2)) { fail(); return; }
struct ref* ref = (struct ref*)array;
if (!ref || !ref->live || start || length != ref->length) { illegal_calls++; return; }
memcpy(ref->bytes, bytes, (size_t)length);
}
/* Старый callback тоже выполняется моделью: отрицательный контроль проверяет реальную прежнюю ошибку, а не падает на NULL vtable. */
static jstring JNICALL legacy_string(JNIEnv* env, const char* text) {
(void)env; check_pending(); legacy_strings++;
struct ref* ref = (struct ref*)new_ref(4);
ref->length = (jsize)strlen(text); memcpy(ref->bytes, text, (size_t)ref->length);
return (jstring)ref;
}
static void JNICALL call(JNIEnv* env, jobject object, jmethodID mid, ...) {
(void)env; check_pending(); calls++;
if (!object || !((struct ref*)object)->live || !mid) illegal_calls++;
va_list ap; va_start(ap, mid);
int level = va_arg(ap, int);
struct ref* cat = (struct ref*)va_arg(ap, jobject);
struct ref* msg = (struct ref*)va_arg(ap, jobject);
va_end(ap);
payload_ok = level == BLEV_ERROR && cat && msg && cat->kind == 2 && msg->kind == 2 &&
cat->length == (jsize)strlen(category) && msg->length == (jsize)strlen(message) &&
!memcmp(cat->bytes, category, strlen(category)) && !memcmp(msg->bytes, message, strlen(message));
if (fault == CALLBACK) pending = 1;
if (fault == RECURSION && !nested++) jni_on_log(BLEV_ERROR, category, message);
}
static jboolean JNICALL exception_check(JNIEnv* env) { (void)env; return pending ? JNI_TRUE : JNI_FALSE; }
static void JNICALL exception_describe(JNIEnv* env) { (void)env; descriptions++; pending = 0; }
static void JNICALL exception_clear(JNIEnv* env) { (void)env; pending = 0; }
static jint JNICALL get_env(JavaVM* vm, void** output, jint version) {
(void)vm;
if (version != JNI_VERSION_1_6) illegal_calls++;
if (fault == GET_ENV) return JNI_EVERSION;
if (!already_attached) return JNI_EDETACHED;
*output = &test_env; return JNI_OK;
}
static jint JNICALL attach(JavaVM* vm, void** output, void* args) {
(void)vm; (void)args; attaches++;
if (fault == ATTACH) return JNI_ERR;
*output = &test_env; return JNI_OK;
}
static jint JNICALL detach(JavaVM* vm) { (void)vm; detaches++; return fault == DETACH ? JNI_ERR : JNI_OK; }
static const struct JNINativeInterface_ functions = {
.NewLocalRef = local_ref, .DeleteLocalRef = delete_ref, .GetObjectClass = object_class, .GetMethodID = method,
.NewByteArray = byte_array, .SetByteArrayRegion = copy_bytes, .NewStringUTF = legacy_string, .CallVoidMethod = call,
.ExceptionCheck = exception_check, .ExceptionDescribe = exception_describe, .ExceptionClear = exception_clear
};
static const struct JNIInvokeInterface_ vm_functions = { .GetEnv = get_env, .AttachCurrentThread = attach, .DetachCurrentThread = detach };
static JavaVM test_vm = &vm_functions;
static void reset(enum fault next) {
memset(refs, 0, sizeof(refs)); memset(&global_ref, 0, sizeof(global_ref)); global_ref.live = 1;
pending = illegal_calls = calls = attaches = detaches = descriptions = diagnostics = nested = 0;
arrays = copies = legacy_strings = payload_ok = 0;
last_diagnostic[0] = method_signature[0] = 0;
throw_on_failure = already_attached = 1;
fault = next; test_env = &functions; g_jvm = &test_vm; g_log_callback = (jobject)&global_ref;
}
static int success(int detached) {
reset(OK); already_attached = !detached;
jni_on_log(BLEV_ERROR, category, message);
CHECK(calls == 1 && payload_ok && !legacy_strings && !illegal_calls && !live_refs() && !pending && !diagnostics);
CHECK(attaches == detached && detaches == detached);
CHECK(!strcmp(method_signature, "(I[B[B)V"));
return 0;
}
static int failure(enum fault next, const char* phase, int throws) {
reset(next); throw_on_failure = throws;
if (next == ATTACH || next == DETACH) already_attached = 0;
jni_on_log(BLEV_ERROR, category, message);
CHECK(diagnostics && strstr(last_diagnostic, phase));
CHECK(!illegal_calls && !live_refs() && !pending);
CHECK(calls == (next == CALLBACK || next == DETACH));
CHECK(detaches == (next == DETACH));
CHECK(descriptions == (throws && next != GET_ENV && next != ATTACH && next != DETACH));
/* Отказ не отравляет следующий callback и не оставляет защиту рекурсии включённой. */
fault = OK; already_attached = 1;
jni_on_log(BLEV_ERROR, category, message);
CHECK(payload_ok && !illegal_calls && !live_refs() && !pending);
return 0;
}
static int preexisting_exception(void) {
reset(OK); pending = 1;
jni_on_log(BLEV_ERROR, category, message);
CHECK(pending && !descriptions && !illegal_calls && !calls && diagnostics && !live_refs());
return 0;
}
static int recursive_callback(void) {
reset(RECURSION);
jni_on_log(BLEV_ERROR, category, message);
CHECK(calls == 1 && diagnostics && strstr(last_diagnostic, "recursive") && !illegal_calls && !live_refs());
return 0;
}
static int replaced_callback(void) {
reset(REPLACE);
jni_on_log(BLEV_ERROR, category, message);
CHECK(calls == 1 && payload_ok && !illegal_calls && !live_refs());
return 0;
}
static int disabled_and_invalid(void) {
reset(OK); g_log_callback = NULL;
jni_on_log(BLEV_ERROR, category, message);
CHECK(!calls && !diagnostics && !live_refs());
reset(OK); g_jvm = NULL;
jni_on_log(BLEV_ERROR, category, message);
CHECK(!calls && diagnostics && !live_refs());
reset(OK);
jni_on_log(BLEV_ERROR, NULL, message);
CHECK(!calls && diagnostics && !live_refs());
reset(OK);
jni_on_log(BLEV_ERROR, category, NULL);
CHECK(!calls && diagnostics && !live_refs());
return 0;
}
int main(void) {
struct { enum fault fault; const char* phase; } cases[] = {
{GET_ENV, "GetEnv"}, {ATTACH, "AttachCurrentThread"}, {LOCAL_REF, "NewLocalRef"},
{OBJECT_CLASS, "GetObjectClass"}, {METHOD, "GetMethodID"}, {CATEGORY, "category"}, {MESSAGE, "message"},
{COPY_CATEGORY, "category"}, {COPY_MESSAGE, "message"}, {CALLBACK, "onNativeLog"}, {DETACH, "DetachCurrentThread"}
};
int failed = success(0) + success(1) + preexisting_exception() + recursive_callback() + replaced_callback() + disabled_and_invalid();
int count = 6;
for (unsigned i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) {
failed += failure(cases[i].fault, cases[i].phase, 1); count++;
if (cases[i].fault >= LOCAL_REF && cases[i].fault <= MESSAGE) {
failed += failure(cases[i].fault, cases[i].phase, 0); count++;
}
}
printf("JNI log scenarios: %d passed, %d failed (%d total)\n", count - failed, failed, count);
return failed ? 1 : 0;
}