From 3ffa7958bc01ee0bc7fabccfbc3de6e4af94ea0b Mon Sep 17 00:00:00 2001 From: PiX <69745008+pixincreate@users.noreply.github.com> Date: Thu, 16 Jul 2026 00:49:00 +0530 Subject: [PATCH 1/8] Define GrapheneOS exec-spawn replay contract --- native/src/core/zygisk/exec_spawn_replay.hpp | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 native/src/core/zygisk/exec_spawn_replay.hpp diff --git a/native/src/core/zygisk/exec_spawn_replay.hpp b/native/src/core/zygisk/exec_spawn_replay.hpp new file mode 100644 index 0000000000000..b9e0d38be6e34 --- /dev/null +++ b/native/src/core/zygisk/exec_spawn_replay.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include +#include +#include + +namespace zygisk { + +inline constexpr std::size_t kGrapheneOsNativeForkFlagsIndex = 1; +inline constexpr std::int64_t kGrapheneOsUseZygoteSpawning = 1LL << 3; + +inline bool is_grapheneos_exec_spawn_replay_contract( + std::int64_t native_fork_flags, std::span fds_to_close) { + return (native_fork_flags & kGrapheneOsUseZygoteSpawning) == 0 && + fds_to_close.size() == 2 && + fds_to_close[0] == -1 && + fds_to_close[1] == -1; +} + +} From 80fc561bed75631470633fd2da1f87006e45e8d7 Mon Sep 17 00:00:00 2001 From: PiX <69745008+pixincreate@users.noreply.github.com> Date: Thu, 16 Jul 2026 00:49:09 +0530 Subject: [PATCH 2/8] Add in-place Zygisk specialization path --- native/src/core/zygisk/module.cpp | 46 +++++++++++++++++++++++++++++++ native/src/core/zygisk/module.hpp | 6 ++++ 2 files changed, 52 insertions(+) diff --git a/native/src/core/zygisk/module.cpp b/native/src/core/zygisk/module.cpp index 5109ee473b0c7..fb289ef7d2e34 100644 --- a/native/src/core/zygisk/module.cpp +++ b/native/src/core/zygisk/module.cpp @@ -1,12 +1,14 @@ #include #include #include +#include #include #include #include "zygisk.hpp" +#include "exec_spawn_replay.hpp" #include "module.hpp" using namespace std; @@ -293,6 +295,28 @@ void ZygiskContext::sanitize_fds() { } } +bool is_grapheneos_exec_spawn_replay( + JNIEnv *env, jlongArray grapheneos_extra_args, jintArray fds_to_close) { + if (grapheneos_extra_args == nullptr || fds_to_close == nullptr || + env->GetArrayLength(grapheneos_extra_args) <= + static_cast(zygisk::kGrapheneOsNativeForkFlagsIndex) || + env->GetArrayLength(fds_to_close) != 2) { + return false; + } + + jlong native_fork_flags = 0; + env->GetLongArrayRegion( + grapheneos_extra_args, + static_cast(zygisk::kGrapheneOsNativeForkFlagsIndex), + 1, + &native_fork_flags); + std::array close_fds{}; + env->GetIntArrayRegion( + fds_to_close, 0, static_cast(close_fds.size()), close_fds.data()); + return zygisk::is_grapheneos_exec_spawn_replay_contract( + native_fork_flags, std::span(close_fds.data(), close_fds.size())); +} + bool ZygiskContext::exempt_fd(int fd) { if ((flags & POST_SPECIALIZE) || (flags & SKIP_CLOSE_LOG_PIPE)) return true; @@ -322,6 +346,10 @@ void ZygiskContext::fork_pre() { if (!is_child()) return; + record_open_fds(); +} + +void ZygiskContext::record_open_fds() { // Record all open fds auto dir = xopen_dir("/proc/self/fd"); for (dirent *entry; (entry = xreaddir(dir.get()));) { @@ -498,3 +526,21 @@ void ZygiskContext::nativeForkAndSpecialize_post() { } fork_post(); } + +void ZygiskContext::nativeForkAndSpecialize_in_place_pre() { + process = env->GetStringUTFChars(args.app->nice_name, nullptr); + ZLOGV("pre forkAndSpecialize in-place [%s]\n", process); + flags |= APP_FORK_AND_SPECIALIZE; + record_open_fds(); + app_specialize_pre(); + sanitize_fds(); +} + +void ZygiskContext::nativeForkAndSpecialize_in_place_post(bool specialized) { + if (specialized) { + ZLOGV("post forkAndSpecialize in-place [%s]\n", process); + app_specialize_post(); + } else { + env->ReleaseStringUTFChars(args.app->nice_name, process); + } +} diff --git a/native/src/core/zygisk/module.hpp b/native/src/core/zygisk/module.hpp index 5a303d35df2df..52f89d01462f9 100644 --- a/native/src/core/zygisk/module.hpp +++ b/native/src/core/zygisk/module.hpp @@ -211,6 +211,9 @@ struct ZygiskModule { extern ZygiskContext *g_ctx; extern int (*old_fork)(void); +bool is_grapheneos_exec_spawn_replay( + JNIEnv *env, jlongArray grapheneos_extra_args, jintArray fds_to_close); + enum : uint32_t { POST_SPECIALIZE = (1u << 0), APP_FORK_AND_SPECIALIZE = (1u << 1), @@ -268,8 +271,11 @@ struct ZygiskContext { DCL_PRE_POST(nativeForkAndSpecialize) DCL_PRE_POST(nativeSpecializeAppProcess) DCL_PRE_POST(nativeForkSystemServer) + void nativeForkAndSpecialize_in_place_pre(); + void nativeForkAndSpecialize_in_place_post(bool specialized); int get_module_info(int uid, rust::Vec &fds); + void record_open_fds(); void sanitize_fds(); bool exempt_fd(int fd); bool can_exempt_fd() const; From 9b781e5ff871165b9408e16c6c669541d6e324e1 Mon Sep 17 00:00:00 2001 From: PiX <69745008+pixincreate@users.noreply.github.com> Date: Thu, 16 Jul 2026 00:49:19 +0530 Subject: [PATCH 3/8] Use forkless path for GrapheneOS exec replay --- native/src/core/zygisk/gen_jni_hooks.py | 36 ++++- native/src/core/zygisk/jni_hooks.hpp | 14 +- .../core/zygisk/test_grapheneos_exec_spawn.py | 135 ++++++++++++++++++ 3 files changed, 178 insertions(+), 7 deletions(-) create mode 100644 native/src/core/zygisk/test_grapheneos_exec_spawn.py diff --git a/native/src/core/zygisk/gen_jni_hooks.py b/native/src/core/zygisk/gen_jni_hooks.py index d33fbbc23e903..a941df2847ff5 100755 --- a/native/src/core/zygisk/gen_jni_hooks.py +++ b/native/src/core/zygisk/gen_jni_hooks.py @@ -38,8 +38,8 @@ def cpp(self) -> str: class Anon(Argument): cnt = 0 - def __init__(self, type: JType): - super().__init__(f"_{Anon.cnt}", type) + def __init__(self, type: JType, name=None): + super().__init__(name or f"_{Anon.cnt}", type) Anon.cnt += 1 @@ -131,6 +131,34 @@ def body(self, orig_fn_ptr: str): return decl +class ForkGrapheneOsC(ForkApp): + def body(self, orig_fn_ptr: str): + decl = "" + decl += ind(3) + self.init_args() + for a in self.args: + if a.set_arg: + decl += ind(3) + f"args.{a.name} = &{a.name};" + decl += ind(3) + "ZygiskContext ctx(env, &args);" + decl += ind(3) + ( + "if (is_grapheneos_exec_spawn_replay(" + "env, grapheneos_extra_args, fds_to_close)) {" + ) + decl += ind(4) + "ctx.nativeForkAndSpecialize_in_place_pre();" + decl += ind(4) + f"jint result = reinterpret_cast<{self.cpp_fn_type()})>({orig_fn_ptr})(" + decl += ind(5) + self.arg_list_name() + decl += ind(4) + ");" + decl += ind(4) + "ctx.nativeForkAndSpecialize_in_place_post(result == 0);" + decl += ind(4) + "return result;" + decl += ind(3) + "}" + decl += ind(3) + "ctx.nativeForkAndSpecialize_pre();" + decl += ind(3) + f"reinterpret_cast<{self.cpp_fn_type()})>({orig_fn_ptr})(" + decl += ind(4) + self.arg_list_name() + decl += ind(3) + ");" + decl += ind(3) + "ctx.nativeForkAndSpecialize_post();" + decl += ind(3) + "return ctx.pid;" + return decl + + class SpecializeApp(ForkApp): def __init__(self, ver: str, args: list[Argument]): super().__init__(ver, args) @@ -507,10 +535,10 @@ def init_args(self): ) # GrapheneOS C (extra args moved to the first parameter) -fas_grapheneos_c = ForkApp( +fas_grapheneos_c = ForkGrapheneOsC( "grapheneos_c", [ - Anon(jlongArray), + Anon(jlongArray, "grapheneos_extra_args"), uid, gid, gids, diff --git a/native/src/core/zygisk/jni_hooks.hpp b/native/src/core/zygisk/jni_hooks.hpp index b1048b4e3084f..3dc428f03f7ec 100644 --- a/native/src/core/zygisk/jni_hooks.hpp +++ b/native/src/core/zygisk/jni_hooks.hpp @@ -316,7 +316,7 @@ std::array fork_app_methods = {{ "nativeForkAndSpecialize", // ILIILILILLLLZLLZZLLZZZ "([JII[II[[IILjava/lang/String;Ljava/lang/String;[I[IZLjava/lang/String;Ljava/lang/String;ZZ[Ljava/lang/String;[Ljava/lang/String;ZZZ)I", - (void *) +[] [[clang::no_stack_protector]] (JNIEnv *env, jclass clazz, jlongArray _11, jint uid, jint gid, jintArray gids, jint runtime_flags, jobjectArray rlimits, jint mount_external, jstring se_info, jstring nice_name, jintArray fds_to_close, jintArray fds_to_ignore, jboolean is_child_zygote, jstring instruction_set, jstring app_data_dir, jboolean is_top_app, jboolean use_fifo_ui, jobjectArray pkg_data_info_list, jobjectArray whitelisted_data_info_list, jboolean mount_data_dirs, jboolean mount_storage_dirs, jboolean mount_sysprop_overrides) static -> jint { + (void *) +[] [[clang::no_stack_protector]] (JNIEnv *env, jclass clazz, jlongArray grapheneos_extra_args, jint uid, jint gid, jintArray gids, jint runtime_flags, jobjectArray rlimits, jint mount_external, jstring se_info, jstring nice_name, jintArray fds_to_close, jintArray fds_to_ignore, jboolean is_child_zygote, jstring instruction_set, jstring app_data_dir, jboolean is_top_app, jboolean use_fifo_ui, jobjectArray pkg_data_info_list, jobjectArray whitelisted_data_info_list, jboolean mount_data_dirs, jboolean mount_storage_dirs, jboolean mount_sysprop_overrides) static -> jint { AppSpecializeArgs_v5 args(uid, gid, gids, runtime_flags, rlimits, mount_external, se_info, nice_name, instruction_set, app_data_dir); args.fds_to_ignore = &fds_to_ignore; args.is_child_zygote = &is_child_zygote; @@ -327,9 +327,17 @@ std::array fork_app_methods = {{ args.mount_storage_dirs = &mount_storage_dirs; args.mount_sysprop_overrides = &mount_sysprop_overrides; ZygiskContext ctx(env, &args); + if (is_grapheneos_exec_spawn_replay(env, grapheneos_extra_args, fds_to_close)) { + ctx.nativeForkAndSpecialize_in_place_pre(); + jint result = reinterpret_cast(get_defs()->fork_app_methods[15].fnPtr)( + env, clazz, grapheneos_extra_args, uid, gid, gids, runtime_flags, rlimits, mount_external, se_info, nice_name, fds_to_close, fds_to_ignore, is_child_zygote, instruction_set, app_data_dir, is_top_app, use_fifo_ui, pkg_data_info_list, whitelisted_data_info_list, mount_data_dirs, mount_storage_dirs, mount_sysprop_overrides + ); + ctx.nativeForkAndSpecialize_in_place_post(result == 0); + return result; + } ctx.nativeForkAndSpecialize_pre(); - reinterpret_cast(get_defs()->fork_app_methods[15].fnPtr)( - env, clazz, _11, uid, gid, gids, runtime_flags, rlimits, mount_external, se_info, nice_name, fds_to_close, fds_to_ignore, is_child_zygote, instruction_set, app_data_dir, is_top_app, use_fifo_ui, pkg_data_info_list, whitelisted_data_info_list, mount_data_dirs, mount_storage_dirs, mount_sysprop_overrides + reinterpret_cast(get_defs()->fork_app_methods[15].fnPtr)( + env, clazz, grapheneos_extra_args, uid, gid, gids, runtime_flags, rlimits, mount_external, se_info, nice_name, fds_to_close, fds_to_ignore, is_child_zygote, instruction_set, app_data_dir, is_top_app, use_fifo_ui, pkg_data_info_list, whitelisted_data_info_list, mount_data_dirs, mount_storage_dirs, mount_sysprop_overrides ); ctx.nativeForkAndSpecialize_post(); return ctx.pid; diff --git a/native/src/core/zygisk/test_grapheneos_exec_spawn.py b/native/src/core/zygisk/test_grapheneos_exec_spawn.py new file mode 100644 index 0000000000000..0fe3896d415a0 --- /dev/null +++ b/native/src/core/zygisk/test_grapheneos_exec_spawn.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python3 + +import os +import subprocess +import tempfile +import textwrap +import unittest +from pathlib import Path + + +ZYGISK_DIR = Path(__file__).resolve().parent + + +def generated_method_block(header: str, name: str) -> str: + marker = f"// {name}" + start = header.index(marker) + next_method = header.find("\n // ", start + len(marker)) + end = len(header) if next_method == -1 else next_method + return header[start:end] + + +class GrapheneOsExecSpawnReplayTest(unittest.TestCase): + def test_replay_contract(self) -> None: + source = textwrap.dedent( + """ + #include + #include + #include + + #include "exec_spawn_replay.hpp" + + int main() { + constexpr std::int64_t ordinary_spawning = + zygisk::kGrapheneOsUseZygoteSpawning; + + assert(zygisk::is_grapheneos_exec_spawn_replay_contract( + 0, std::array{-1, -1})); + assert(!zygisk::is_grapheneos_exec_spawn_replay_contract( + ordinary_spawning, std::array{-1, -1})); + assert(!zygisk::is_grapheneos_exec_spawn_replay_contract( + 0, std::array{3, 4})); + assert(!zygisk::is_grapheneos_exec_spawn_replay_contract( + 0, std::array{-1, 4})); + assert(!zygisk::is_grapheneos_exec_spawn_replay_contract( + 0, std::array{-1})); + assert(!zygisk::is_grapheneos_exec_spawn_replay_contract( + 0, std::array{-1, -1, 7})); + } + """ + ) + + with tempfile.TemporaryDirectory() as directory: + temp_dir = Path(directory) + source_path = temp_dir / "exec_spawn_replay_test.cpp" + binary_path = temp_dir / "exec_spawn_replay_test" + source_path.write_text(source) + + subprocess.run( + [ + os.environ.get("CXX", "c++"), + "-std=c++20", + "-Wall", + "-Wextra", + "-Werror", + "-I", + str(ZYGISK_DIR), + str(source_path), + "-o", + str(binary_path), + ], + check=True, + ) + subprocess.run([str(binary_path)], check=True) + + def test_generated_grapheneos_c_replay_path(self) -> None: + with tempfile.TemporaryDirectory() as directory: + subprocess.run( + ["python3", str(ZYGISK_DIR / "gen_jni_hooks.py")], + cwd=directory, + check=True, + ) + header = (Path(directory) / "jni_hooks.hpp").read_text() + + grapheneos_c = generated_method_block( + header, "nativeForkAndSpecialize_grapheneos_c" + ) + self.assertIn( + "([JII[II[[IILjava/lang/String;Ljava/lang/String;[I[IZLjava/lang/String;Ljava/lang/String;ZZ[Ljava/lang/String;[Ljava/lang/String;ZZZ)I", + grapheneos_c, + ) + self.assertIn("jlongArray grapheneos_extra_args", grapheneos_c) + self.assertIn( + "is_grapheneos_exec_spawn_replay(env, grapheneos_extra_args, fds_to_close)", + grapheneos_c, + ) + self.assertIn("ctx.nativeForkAndSpecialize_in_place_pre();", grapheneos_c) + self.assertIn("jint result = reinterpret_cast None: + module = (ZYGISK_DIR / "module.cpp").read_text() + start = module.index( + "void ZygiskContext::nativeForkAndSpecialize_in_place_pre()" + ) + end = module.index( + "void ZygiskContext::nativeForkAndSpecialize_in_place_post", start + ) + in_place_pre = module[start:end] + + snapshot = in_place_pre.index("record_open_fds();") + module_pre = in_place_pre.index("app_specialize_pre();") + sanitize = in_place_pre.index("sanitize_fds();") + self.assertLess(snapshot, module_pre) + self.assertLess(module_pre, sanitize) + self.assertNotIn("fork_pre();", in_place_pre) + self.assertNotIn("old_fork()", in_place_pre) + + +if __name__ == "__main__": + unittest.main() From f53f87f5d534f0555bde523e38521fbc6f96ba53 Mon Sep 17 00:00:00 2001 From: PiX <69745008+pixincreate@users.noreply.github.com> Date: Thu, 16 Jul 2026 23:14:34 +0530 Subject: [PATCH 4/8] Keep Zygisk available for late app zygotes --- native/src/core/zygisk/daemon.rs | 16 ++++++-------- .../core/zygisk/test_grapheneos_exec_spawn.py | 22 +++++++++++++++++++ 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/native/src/core/zygisk/daemon.rs b/native/src/core/zygisk/daemon.rs index 87de2cd1f45e3..cd36be7e23765 100644 --- a/native/src/core/zygisk/daemon.rs +++ b/native/src/core/zygisk/daemon.rs @@ -107,19 +107,17 @@ impl ZygiskState { Ok(()) } - pub fn reset(&mut self, mut restore: bool) { + pub fn reset(&mut self, restore: bool) { if restore { self.start_count = 1; - } else { - self.sockets = (None, None); - self.start_count += 1; - if self.start_count > 3 { - warn!("zygote crashed too many times, rolling-back"); - restore = true; - } + self.set_prop(); + return; } - if restore { + self.sockets = (None, None); + self.start_count += 1; + if self.start_count > 3 { + warn!("zygote crashed too many times, rolling-back"); self.restore_prop(); } else { self.set_prop(); diff --git a/native/src/core/zygisk/test_grapheneos_exec_spawn.py b/native/src/core/zygisk/test_grapheneos_exec_spawn.py index 0fe3896d415a0..856ae3e2aecd8 100644 --- a/native/src/core/zygisk/test_grapheneos_exec_spawn.py +++ b/native/src/core/zygisk/test_grapheneos_exec_spawn.py @@ -130,6 +130,28 @@ def test_in_place_path_preserves_fd_containment_without_forking(self) -> None: self.assertNotIn("fork_pre();", in_place_pre) self.assertNotIn("old_fork()", in_place_pre) + def test_boot_complete_keeps_native_bridge_for_late_zygotes(self) -> None: + daemon = (ZYGISK_DIR / "daemon.rs").read_text() + start = daemon.index("pub fn reset(&mut self") + end = daemon.index("pub fn set_prop(&mut self)", start) + reset = daemon[start:end] + + boot_complete = reset[ + reset.index("if restore {") : reset.index("self.sockets") + ] + self.assertIn("self.set_prop();", boot_complete) + self.assertNotIn("self.restore_prop();", boot_complete) + self.assertRegex(boot_complete, r"self\.set_prop\(\);\s+return;") + + crash_rollback = reset[reset.index("self.start_count += 1;") :] + threshold = crash_rollback.index("if self.start_count > 3 {") + restore = crash_rollback.index("self.restore_prop();", threshold) + fallback = crash_rollback.index("} else {", restore) + rearm = crash_rollback.index("self.set_prop();", fallback) + self.assertLess(threshold, restore) + self.assertLess(restore, fallback) + self.assertLess(fallback, rearm) + if __name__ == "__main__": unittest.main() From b84d2f5af3aec8965149aeabbe0127e2338e3510 Mon Sep 17 00:00:00 2001 From: PiX <69745008+pixincreate@users.noreply.github.com> Date: Thu, 16 Jul 2026 23:56:31 +0530 Subject: [PATCH 5/8] diag: Add diagnostic logging to trace Zygisk hook registration failures Add ZLOGI diagnostics at every critical point in the Zygisk bootstrap chain to identify why RegisterNatives fails for all JNI hook variants on GrapheneOS 17 (Android 17, SDK 37). Logging added at: - hook_plt(): CommitHook success/failure - dlclose hook: when it fires and with what handle - post_native_bridge_load(): runtime_callbacks pointer validity, native bridge reload path - register_jni_methods(): RegisterNatives return code + exception status for EACH variant (name + signature logged) - hook_jni_methods(): enumerate old_methods with signatures+fnPtrs, log hook_count, log each successful HOOKED match - hook_zygote_jni(): enumerate all Zygote native methods with signatures, log each name match (kForkApp/kSpecializeApp/ kForkServer), log final success/failure summary This is diagnostic only - no behavior changes. All logs are tagged with 'diag:' prefix for easy grep. --- native/src/core/zygisk/hook.cpp | 45 +++++++++++++++++++++++++++++---- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/native/src/core/zygisk/hook.cpp b/native/src/core/zygisk/hook.cpp index 6eedfdb4ca10d..6b803dd986135 100644 --- a/native/src/core/zygisk/hook.cpp +++ b/native/src/core/zygisk/hook.cpp @@ -187,7 +187,7 @@ DCL_HOOK_FUNC(static void, android_log_close) { // It should be safe to assume all dlclose's in libnativebridge are for zygisk_loader DCL_HOOK_FUNC(static int, dlclose, void *handle) { if (!g_hook->self_handle) { - ZLOGV("dlclose zygisk_loader\n"); + ZLOGI("diag: dlclose hook fired, handle=%p\n", handle); g_hook->post_native_bridge_load(handle); } return 0; @@ -368,16 +368,24 @@ void HookContext::post_native_bridge_load(void *handle) { return _URC_NO_REASON; }, &arg); - if (!arg.load_native_bridge || !arg.callbacks) + if (!arg.load_native_bridge || !arg.callbacks) { + ZLOGE("diag: post_native_bridge_load FAILED - load_native_bridge=%p callbacks=%p\n", + arg.load_native_bridge, arg.callbacks); return; + } + + ZLOGI("diag: post_native_bridge_load OK - load_native_bridge=%p callbacks=%p\n", + arg.load_native_bridge, arg.callbacks); // Reload the real native bridge if necessary auto nb = get_prop(NBPROP); auto len = sizeof(ZYGISKLDR) - 1; if (nb.size() > len) { + ZLOGI("diag: reloading real native bridge: %s\n", nb.c_str() + len); arg.load_native_bridge(nb.c_str() + len, arg.callbacks); } runtime_callbacks = arg.callbacks; + ZLOGI("diag: runtime_callbacks stored = %p\n", runtime_callbacks); } // ----------------------------------------------------------------- @@ -421,8 +429,11 @@ void HookContext::hook_plt() { PLT_HOOK_REGISTER(android_runtime_dev, android_runtime_inode, strdup); PLT_HOOK_REGISTER_SYM(android_runtime_dev, android_runtime_inode, "__android_log_close", android_log_close); - if (!lsplt::CommitHook()) + if (!lsplt::CommitHook()) { ZLOGE("plt_hook failed\n"); + } else { + ZLOGI("diag: plt_hook committed successfully\n"); + } // Remove unhooked methods std::erase_if(plt_backup, [](auto &t) { return *std::get<3>(t) == nullptr; }); @@ -474,9 +485,15 @@ static void register_jni_methods(JNIEnv *env, jclass clazz, JNIMethods methods) if (!method.fnPtr) continue; // It's normal that the method is not found - if (env->RegisterNatives(clazz, &method, 1) == JNI_ERR || env->ExceptionCheck() == JNI_TRUE) { + jint reg_res = env->RegisterNatives(clazz, &method, 1); + bool has_exception = env->ExceptionCheck() == JNI_TRUE; + if (reg_res == JNI_ERR || has_exception) { + ZLOGI("diag: RegisterNatives FAILED for %s %s (ret=%d, exc=%d)\n", + method.name, method.signature, reg_res, has_exception); env->ExceptionClear(); method.fnPtr = nullptr; + } else { + ZLOGI("diag: RegisterNatives OK for %s %s\n", method.name, method.signature); } } } @@ -486,6 +503,11 @@ int HookContext::hook_jni_methods(JNIEnv *env, jclass clazz, JNIMethods methods) auto o = get_jni_methods(env, clazz); const auto old_methods = span(o.first.get(), o.second); + ZLOGI("diag: hook_jni_methods old_methods count=%zu\n", o.second); + for (const auto &m : old_methods) { + ZLOGI("diag: old: %s %s fnPtr=%p\n", m.name, m.signature, m.fnPtr); + } + // WARNING: the signature field returned from getNativeMethods is in a non-standard format. // DO NOT TRY TO USE IT. This is the reason why we try to call RegisterNatives on every single // provided JNI methods directly to be 100% sure about whether a signature matches or not. @@ -506,7 +528,7 @@ int HookContext::hook_jni_methods(JNIEnv *env, jclass clazz, JNIMethods methods) for (const auto &old_method : old_methods) { if (strcmp(old_method.name, new_method.name) == 0 && strcmp(old_method.signature, new_method.signature) == 0) { - ZLOGV("replace %s %s %p -> %p\n", + ZLOGI("diag: HOOKED %s %s %p -> %p\n", method.name, method.signature, old_method.fnPtr, method.fnPtr); method.fnPtr = old_method.fnPtr; ++hook_count; @@ -518,6 +540,7 @@ int HookContext::hook_jni_methods(JNIEnv *env, jclass clazz, JNIMethods methods) } next_method: } + ZLOGI("diag: hook_jni_methods hook_count=%d\n", hook_count); return hook_count; } @@ -566,6 +589,8 @@ void HookContext::hook_zygote_jni() { ZLOGW("JNIEnv not found\n"); } + ZLOGI("diag: hook_zygote_jni starting, runtime_callbacks=%p\n", runtime_callbacks); + JNINativeMethod missing_method{}; bool replaced_fork_app = false; bool replaced_specialize_app = false; @@ -573,20 +598,27 @@ void HookContext::hook_zygote_jni() { jclass clazz = env->FindClass(kZygote); auto [ptr, count] = get_jni_methods(env, clazz); + ZLOGI("diag: Zygote native methods count=%zu\n", count); + for (const auto methods = span(ptr.get(), count); const auto &method : methods) { + ZLOGI("diag: zygote method: %s %s\n", method.name, method.signature); + } for (const auto methods = span(ptr.get(), count); const auto &method : methods) { if (strcmp(method.name, kForkApp) == 0) { + ZLOGI("diag: matched kForkApp=%s\n", method.name); if (hook_jni_methods(env, clazz, fork_app_methods) == 0) { missing_method = method; break; } replaced_fork_app = true; } else if (strcmp(method.name, kSpecializeApp) == 0) { + ZLOGI("diag: matched kSpecializeApp=%s\n", method.name); if (hook_jni_methods(env, clazz, specialize_app_methods) == 0) { missing_method = method; break; } replaced_specialize_app = true; } else if (strcmp(method.name, kForkServer) == 0) { + ZLOGI("diag: matched kForkServer=%s\n", method.name); if (hook_jni_methods(env, clazz, fork_server_methods) == 0) { missing_method = method; break; @@ -605,6 +637,9 @@ void HookContext::hook_zygote_jni() { ranges::for_each(fork_app_methods, [](auto &m) { m.fnPtr = nullptr; }); ranges::for_each(specialize_app_methods, [](auto &m) { m.fnPtr = nullptr; }); ranges::for_each(fork_server_methods, [](auto &m) { m.fnPtr = nullptr; }); + } else { + ZLOGI("diag: hook_zygote_jni OK - fork_app=%d specialize_app=%d fork_server=%d\n", + replaced_fork_app, replaced_specialize_app, replaced_fork_server); } } From c70af40be2ff159f6f4c0d436007dba3f6e549b4 Mon Sep 17 00:00:00 2001 From: PiX <69745008+pixincreate@users.noreply.github.com> Date: Sat, 25 Jul 2026 20:58:03 +0530 Subject: [PATCH 6/8] diag: Add grapheneos replay predicate/in-place tracer markers --- native/src/core/zygisk/module.cpp | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/native/src/core/zygisk/module.cpp b/native/src/core/zygisk/module.cpp index fb289ef7d2e34..37299327143e0 100644 --- a/native/src/core/zygisk/module.cpp +++ b/native/src/core/zygisk/module.cpp @@ -301,6 +301,7 @@ bool is_grapheneos_exec_spawn_replay( env->GetArrayLength(grapheneos_extra_args) <= static_cast(zygisk::kGrapheneOsNativeForkFlagsIndex) || env->GetArrayLength(fds_to_close) != 2) { + ZLOGD("diag: replay false invalid array shape\n"); return false; } @@ -313,8 +314,14 @@ bool is_grapheneos_exec_spawn_replay( std::array close_fds{}; env->GetIntArrayRegion( fds_to_close, 0, static_cast(close_fds.size()), close_fds.data()); - return zygisk::is_grapheneos_exec_spawn_replay_contract( + const bool replay = zygisk::is_grapheneos_exec_spawn_replay_contract( native_fork_flags, std::span(close_fds.data(), close_fds.size())); + ZLOGD("diag: replay %s flags=%lld fds=[%d,%d]\n", + replay ? "true" : "false", + static_cast(native_fork_flags), + static_cast(close_fds[0]), + static_cast(close_fds[1])); + return replay; } bool ZygiskContext::exempt_fd(int fd) { @@ -531,6 +538,7 @@ void ZygiskContext::nativeForkAndSpecialize_in_place_pre() { process = env->GetStringUTFChars(args.app->nice_name, nullptr); ZLOGV("pre forkAndSpecialize in-place [%s]\n", process); flags |= APP_FORK_AND_SPECIALIZE; + ZLOGD("diag: in_place_pre entry\n"); record_open_fds(); app_specialize_pre(); sanitize_fds(); @@ -543,4 +551,5 @@ void ZygiskContext::nativeForkAndSpecialize_in_place_post(bool specialized) { } else { env->ReleaseStringUTFChars(args.app->nice_name, process); } + ZLOGD("diag: in_place_post specialized=%s\n", specialized ? "true" : "false"); } From 1da659e7bd9aa3d3c1cfa405cae5482904fc765c Mon Sep 17 00:00:00 2001 From: PiX <69745008+pixincreate@users.noreply.github.com> Date: Wed, 12 Aug 2026 19:25:39 +0530 Subject: [PATCH 7/8] diag: bump grapheneos replay markers to info level LOGD is compiled out in release builds (MAGISK_DEBUG=0), so the replay predicate and in-place markers were invisible in tester logs. Use LOGI so the markers survive release artifacts. --- native/src/core/zygisk/module.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/native/src/core/zygisk/module.cpp b/native/src/core/zygisk/module.cpp index 37299327143e0..dff227bd9c82c 100644 --- a/native/src/core/zygisk/module.cpp +++ b/native/src/core/zygisk/module.cpp @@ -301,7 +301,7 @@ bool is_grapheneos_exec_spawn_replay( env->GetArrayLength(grapheneos_extra_args) <= static_cast(zygisk::kGrapheneOsNativeForkFlagsIndex) || env->GetArrayLength(fds_to_close) != 2) { - ZLOGD("diag: replay false invalid array shape\n"); + ZLOGI("diag: replay false invalid array shape\n"); return false; } @@ -316,7 +316,7 @@ bool is_grapheneos_exec_spawn_replay( fds_to_close, 0, static_cast(close_fds.size()), close_fds.data()); const bool replay = zygisk::is_grapheneos_exec_spawn_replay_contract( native_fork_flags, std::span(close_fds.data(), close_fds.size())); - ZLOGD("diag: replay %s flags=%lld fds=[%d,%d]\n", + ZLOGI("diag: replay %s flags=%lld fds=[%d,%d]\n", replay ? "true" : "false", static_cast(native_fork_flags), static_cast(close_fds[0]), @@ -538,7 +538,7 @@ void ZygiskContext::nativeForkAndSpecialize_in_place_pre() { process = env->GetStringUTFChars(args.app->nice_name, nullptr); ZLOGV("pre forkAndSpecialize in-place [%s]\n", process); flags |= APP_FORK_AND_SPECIALIZE; - ZLOGD("diag: in_place_pre entry\n"); + ZLOGI("diag: in_place_pre entry\n"); record_open_fds(); app_specialize_pre(); sanitize_fds(); @@ -551,5 +551,5 @@ void ZygiskContext::nativeForkAndSpecialize_in_place_post(bool specialized) { } else { env->ReleaseStringUTFChars(args.app->nice_name, process); } - ZLOGD("diag: in_place_post specialized=%s\n", specialized ? "true" : "false"); + ZLOGI("diag: in_place_post specialized=%s\n", specialized ? "true" : "false"); } From 7873b295dbf6d25b2d174615db29c7e5e22219ec Mon Sep 17 00:00:00 2001 From: PiX <69745008+pixincreate@users.noreply.github.com> Date: Fri, 14 Aug 2026 13:37:02 +0530 Subject: [PATCH 8/8] zygisk: remove temporary grapheneos diagnostic markers Runtime validation on Pixel Tablet / GrapheneOS 17 with secure app spawning enabled confirmed the replay path fires and completes (diag: replay true flags=0 fds=[-1,-1], in_place_pre entry, in_place_post specialized=true, no crashes). Strip the temporary instrumentation inserted to trace hook registration and the in-place replay branch so production builds don't spam logcat at INFO level on every GrapheneOS app launch. --- native/src/core/zygisk/hook.cpp | 39 ++----------------------------- native/src/core/zygisk/module.cpp | 11 +-------- 2 files changed, 3 insertions(+), 47 deletions(-) diff --git a/native/src/core/zygisk/hook.cpp b/native/src/core/zygisk/hook.cpp index 6b803dd986135..bcbc2cce05781 100644 --- a/native/src/core/zygisk/hook.cpp +++ b/native/src/core/zygisk/hook.cpp @@ -187,7 +187,6 @@ DCL_HOOK_FUNC(static void, android_log_close) { // It should be safe to assume all dlclose's in libnativebridge are for zygisk_loader DCL_HOOK_FUNC(static int, dlclose, void *handle) { if (!g_hook->self_handle) { - ZLOGI("diag: dlclose hook fired, handle=%p\n", handle); g_hook->post_native_bridge_load(handle); } return 0; @@ -369,23 +368,16 @@ void HookContext::post_native_bridge_load(void *handle) { }, &arg); if (!arg.load_native_bridge || !arg.callbacks) { - ZLOGE("diag: post_native_bridge_load FAILED - load_native_bridge=%p callbacks=%p\n", - arg.load_native_bridge, arg.callbacks); return; } - ZLOGI("diag: post_native_bridge_load OK - load_native_bridge=%p callbacks=%p\n", - arg.load_native_bridge, arg.callbacks); - // Reload the real native bridge if necessary auto nb = get_prop(NBPROP); auto len = sizeof(ZYGISKLDR) - 1; if (nb.size() > len) { - ZLOGI("diag: reloading real native bridge: %s\n", nb.c_str() + len); arg.load_native_bridge(nb.c_str() + len, arg.callbacks); } runtime_callbacks = arg.callbacks; - ZLOGI("diag: runtime_callbacks stored = %p\n", runtime_callbacks); } // ----------------------------------------------------------------- @@ -431,8 +423,6 @@ void HookContext::hook_plt() { if (!lsplt::CommitHook()) { ZLOGE("plt_hook failed\n"); - } else { - ZLOGI("diag: plt_hook committed successfully\n"); } // Remove unhooked methods @@ -485,15 +475,10 @@ static void register_jni_methods(JNIEnv *env, jclass clazz, JNIMethods methods) if (!method.fnPtr) continue; // It's normal that the method is not found - jint reg_res = env->RegisterNatives(clazz, &method, 1); - bool has_exception = env->ExceptionCheck() == JNI_TRUE; - if (reg_res == JNI_ERR || has_exception) { - ZLOGI("diag: RegisterNatives FAILED for %s %s (ret=%d, exc=%d)\n", - method.name, method.signature, reg_res, has_exception); + if (env->RegisterNatives(clazz, &method, 1) == JNI_ERR || + env->ExceptionCheck() == JNI_TRUE) { env->ExceptionClear(); method.fnPtr = nullptr; - } else { - ZLOGI("diag: RegisterNatives OK for %s %s\n", method.name, method.signature); } } } @@ -503,11 +488,6 @@ int HookContext::hook_jni_methods(JNIEnv *env, jclass clazz, JNIMethods methods) auto o = get_jni_methods(env, clazz); const auto old_methods = span(o.first.get(), o.second); - ZLOGI("diag: hook_jni_methods old_methods count=%zu\n", o.second); - for (const auto &m : old_methods) { - ZLOGI("diag: old: %s %s fnPtr=%p\n", m.name, m.signature, m.fnPtr); - } - // WARNING: the signature field returned from getNativeMethods is in a non-standard format. // DO NOT TRY TO USE IT. This is the reason why we try to call RegisterNatives on every single // provided JNI methods directly to be 100% sure about whether a signature matches or not. @@ -528,8 +508,6 @@ int HookContext::hook_jni_methods(JNIEnv *env, jclass clazz, JNIMethods methods) for (const auto &old_method : old_methods) { if (strcmp(old_method.name, new_method.name) == 0 && strcmp(old_method.signature, new_method.signature) == 0) { - ZLOGI("diag: HOOKED %s %s %p -> %p\n", - method.name, method.signature, old_method.fnPtr, method.fnPtr); method.fnPtr = old_method.fnPtr; ++hook_count; // Break 2 levels of for loop @@ -540,7 +518,6 @@ int HookContext::hook_jni_methods(JNIEnv *env, jclass clazz, JNIMethods methods) } next_method: } - ZLOGI("diag: hook_jni_methods hook_count=%d\n", hook_count); return hook_count; } @@ -589,8 +566,6 @@ void HookContext::hook_zygote_jni() { ZLOGW("JNIEnv not found\n"); } - ZLOGI("diag: hook_zygote_jni starting, runtime_callbacks=%p\n", runtime_callbacks); - JNINativeMethod missing_method{}; bool replaced_fork_app = false; bool replaced_specialize_app = false; @@ -598,27 +573,20 @@ void HookContext::hook_zygote_jni() { jclass clazz = env->FindClass(kZygote); auto [ptr, count] = get_jni_methods(env, clazz); - ZLOGI("diag: Zygote native methods count=%zu\n", count); - for (const auto methods = span(ptr.get(), count); const auto &method : methods) { - ZLOGI("diag: zygote method: %s %s\n", method.name, method.signature); - } for (const auto methods = span(ptr.get(), count); const auto &method : methods) { if (strcmp(method.name, kForkApp) == 0) { - ZLOGI("diag: matched kForkApp=%s\n", method.name); if (hook_jni_methods(env, clazz, fork_app_methods) == 0) { missing_method = method; break; } replaced_fork_app = true; } else if (strcmp(method.name, kSpecializeApp) == 0) { - ZLOGI("diag: matched kSpecializeApp=%s\n", method.name); if (hook_jni_methods(env, clazz, specialize_app_methods) == 0) { missing_method = method; break; } replaced_specialize_app = true; } else if (strcmp(method.name, kForkServer) == 0) { - ZLOGI("diag: matched kForkServer=%s\n", method.name); if (hook_jni_methods(env, clazz, fork_server_methods) == 0) { missing_method = method; break; @@ -637,9 +605,6 @@ void HookContext::hook_zygote_jni() { ranges::for_each(fork_app_methods, [](auto &m) { m.fnPtr = nullptr; }); ranges::for_each(specialize_app_methods, [](auto &m) { m.fnPtr = nullptr; }); ranges::for_each(fork_server_methods, [](auto &m) { m.fnPtr = nullptr; }); - } else { - ZLOGI("diag: hook_zygote_jni OK - fork_app=%d specialize_app=%d fork_server=%d\n", - replaced_fork_app, replaced_specialize_app, replaced_fork_server); } } diff --git a/native/src/core/zygisk/module.cpp b/native/src/core/zygisk/module.cpp index dff227bd9c82c..fb289ef7d2e34 100644 --- a/native/src/core/zygisk/module.cpp +++ b/native/src/core/zygisk/module.cpp @@ -301,7 +301,6 @@ bool is_grapheneos_exec_spawn_replay( env->GetArrayLength(grapheneos_extra_args) <= static_cast(zygisk::kGrapheneOsNativeForkFlagsIndex) || env->GetArrayLength(fds_to_close) != 2) { - ZLOGI("diag: replay false invalid array shape\n"); return false; } @@ -314,14 +313,8 @@ bool is_grapheneos_exec_spawn_replay( std::array close_fds{}; env->GetIntArrayRegion( fds_to_close, 0, static_cast(close_fds.size()), close_fds.data()); - const bool replay = zygisk::is_grapheneos_exec_spawn_replay_contract( + return zygisk::is_grapheneos_exec_spawn_replay_contract( native_fork_flags, std::span(close_fds.data(), close_fds.size())); - ZLOGI("diag: replay %s flags=%lld fds=[%d,%d]\n", - replay ? "true" : "false", - static_cast(native_fork_flags), - static_cast(close_fds[0]), - static_cast(close_fds[1])); - return replay; } bool ZygiskContext::exempt_fd(int fd) { @@ -538,7 +531,6 @@ void ZygiskContext::nativeForkAndSpecialize_in_place_pre() { process = env->GetStringUTFChars(args.app->nice_name, nullptr); ZLOGV("pre forkAndSpecialize in-place [%s]\n", process); flags |= APP_FORK_AND_SPECIALIZE; - ZLOGI("diag: in_place_pre entry\n"); record_open_fds(); app_specialize_pre(); sanitize_fds(); @@ -551,5 +543,4 @@ void ZygiskContext::nativeForkAndSpecialize_in_place_post(bool specialized) { } else { env->ReleaseStringUTFChars(args.app->nice_name, process); } - ZLOGI("diag: in_place_post specialized=%s\n", specialized ? "true" : "false"); }