diff --git a/native/src/exported_sym.txt b/native/src/exported_sym.txt index 9e50222b2..80dc508b4 100644 --- a/native/src/exported_sym.txt +++ b/native/src/exported_sym.txt @@ -1,3 +1,4 @@ { zygisk_inject_entry; + unload_first_stage; }; diff --git a/native/src/zygisk/entry.cpp b/native/src/zygisk/entry.cpp index 1fa554023..5b306bbd8 100644 --- a/native/src/zygisk/entry.cpp +++ b/native/src/zygisk/entry.cpp @@ -43,13 +43,10 @@ static void zygisk_cleanup_wait() { } } -static void *unload_first_stage(void *) { - // Wait 10us to make sure 1st stage is done - timespec ts = { .tv_sec = 0, .tv_nsec = 10000L }; - nanosleep(&ts, nullptr); +extern "C" void unload_first_stage() { + ZLOGD("unloading first stage\n"); unmap_all(HIJACK_BIN); xumount2(HIJACK_BIN, MNT_DETACH); - return nullptr; } extern "C" void zygisk_inject_entry(void *handle) { @@ -70,7 +67,6 @@ extern "C" void zygisk_inject_entry(void *handle) { unsetenv(MAGISKTMP_ENV); sanitize_environ(); hook_functions(); - new_daemon_thread(&unload_first_stage, nullptr); } // The following code runs in zygote/app process diff --git a/native/src/zygisk/hook.cpp b/native/src/zygisk/hook.cpp index 9a95bc62b..d38dfa7ad 100644 --- a/native/src/zygisk/hook.cpp +++ b/native/src/zygisk/hook.cpp @@ -259,6 +259,29 @@ DCL_HOOK_FUNC(int, selinux_android_setcontext, return old_selinux_android_setcontext(uid, isSystemServer, seinfo, pkgname); } +dev_t art_dev = 0; +ino_t art_inode = 0; +pthread_t main_thread = 0; +DCL_HOOK_FUNC(int, pthread_attr_destroy, void* target) { + int res = old_pthread_attr_destroy((pthread_attr_t *)target); + + if (main_thread != pthread_self()) { + return res; + } + + ZLOGD("pthread_attr_destroy(%p)\n", target); + + lsplt::RegisterHook(art_dev, art_inode, "pthread_attr_destroy", reinterpret_cast(*old_pthread_attr_destroy), nullptr); + lsplt::CommitHook(); + + // Directly call `dlclose` will crash because when `dlclose` returns, CPU will try to execute code at an + // unmapped address. Because both `pthread_attr_destroy` and `dlclose` have same + // parameter types (a pointer) and return type (an int), the way they treat arguments and results + // are the same. We can use `musttail` to let the compiler reuse our stack frame and thus + // dlclose will directly return to the caller of `pthread_attr_destroy` + [[clang::musttail]] return dlclose(self_handle); +} + #undef DCL_HOOK_FUNC // ----------------------------------------------------------------- @@ -646,7 +669,23 @@ void HookContext::unload_zygisk() { m.clearApi(); } - new_daemon_thread(reinterpret_cast(&dlclose), self_handle); + // We cannot directly `dlclose` ourselves here, otherwise when `dlclose` returns, it will return + // to our code which have been unmapped, causing a segment fault. Instead, we hook + // `pthread_attr_destroy` which will be called when VM daemon threads start and + // do some magic. Check comment in our replacement of the function for more info. + for (auto &map : lsplt::MapInfo::Scan()) { + if (map.path.ends_with("/libart.so")) { + ZLOGD("hook pthread_attr_destroy\n"); + art_inode = map.inode; + art_dev = map.dev; + main_thread = pthread_self(); + lsplt::RegisterHook(art_dev, art_inode, "pthread_attr_destroy", + reinterpret_cast(new_pthread_attr_destroy), + reinterpret_cast(&old_pthread_attr_destroy)); + lsplt::CommitHook(); + break; + } + } } } diff --git a/native/src/zygisk/loader.c b/native/src/zygisk/loader.c index bcf385750..363d4c70d 100644 --- a/native/src/zygisk/loader.c +++ b/native/src/zygisk/loader.c @@ -10,7 +10,7 @@ #endif __attribute__((constructor)) -static void zygisk_loader() { +static void zygisk_loader(void) { android_dlextinfo info = { .flags = ANDROID_DLEXT_FORCE_LOAD }; @@ -20,5 +20,9 @@ static void zygisk_loader() { if (entry) { entry(handle); } + void (*unload)(void) = dlsym(handle, "unload_first_stage"); + if (unload) { + __attribute__((musttail)) return unload(); + } } }