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/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; +} + +} 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/hook.cpp b/native/src/core/zygisk/hook.cpp index 6eedfdb4ca10d..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) { - ZLOGV("dlclose zygisk_loader\n"); g_hook->post_native_bridge_load(handle); } return 0; @@ -368,8 +367,9 @@ 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) { return; + } // Reload the real native bridge if necessary auto nb = get_prop(NBPROP); @@ -421,8 +421,9 @@ 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"); + } // Remove unhooked methods std::erase_if(plt_backup, [](auto &t) { return *std::get<3>(t) == nullptr; }); @@ -474,7 +475,8 @@ 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) { + if (env->RegisterNatives(clazz, &method, 1) == JNI_ERR || + env->ExceptionCheck() == JNI_TRUE) { env->ExceptionClear(); method.fnPtr = nullptr; } @@ -506,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) { - ZLOGV("replace %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 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/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; 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..856ae3e2aecd8 --- /dev/null +++ b/native/src/core/zygisk/test_grapheneos_exec_spawn.py @@ -0,0 +1,157 @@ +#!/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) + + 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()