From a477233e19cf85b41b8987d4a24df8c4f4243e6f Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Thu, 3 Sep 2026 17:17:34 +0200 Subject: [PATCH] Remove the traces our own unloading leaves behind clean_linker_trace() in run_modules_post asked for "jit-cache-zygisk". The daemon creates the module memfd as "zygisk-module", and has since 5e43a06 renamed it back from the former; b4d31ab caught the same stale name in spoof_virtual_maps but never opened this call site. So wherever a module declines to unload -- 25 processes in one boot here, system_server among them -- the linker kept an soinfo record reading /memfd:zygisk-module (deleted). spoof_virtual_maps hides the mapping; the record stays visible to anyone walking the list. Releasing an soinfo returns its block to the linker's slab allocator, which is strictly LIFO, so the block released last is reissued first. Releasing front to back hands them back in reverse, and dl_iterate_phdr enumerates in allocation order -- an object on a lower block than the one before it is a step backwards that a process which never unloaded anything cannot show. Release the highest block first, in dropSoPath and in the module loop, and the reuse stays ascending. Collect before releasing too: soinfo_free memsets the block, so walking the list while releasing read a zeroed next and stopped after the first hit. The self-unmap was short by the .bss. The ptracer sums the mappings carrying our pathname, and bionic maps the tail of the last PT_LOAD as an anonymous zeromap with no file behind it -- 8192 bytes on arm64, one page on the 32-bit ABIs, stranded in every process we inject into. dropSoPath now reports soinfo::size, and hook_entry prefers it once the base agrees. That sum identifies the library by name once and collects by device and inode after, so a "(deleted)" suffix cannot widen the range we unmap. Carry the whole .llvm suffix: Android 17's twenty-six characters filled the old buffer and left it unterminated, so solinker failed to resolve and the solist path degraded silently. Spoofed regions are named rather than left anonymous. --- loader/src/include/linker_soinfo.h | 2 + loader/src/include/solist.hpp | 11 ++- loader/src/injector/clean.cpp | 13 +++- loader/src/injector/hook.cpp | 19 ++++- loader/src/injector/module.cpp | 11 ++- loader/src/injector/solist.cpp | 107 +++++++++++++++++++---------- loader/src/injector/zygisk.hpp | 3 +- loader/src/ptracer/ptracer.cpp | 22 +++++- 8 files changed, 142 insertions(+), 46 deletions(-) diff --git a/loader/src/include/linker_soinfo.h b/loader/src/include/linker_soinfo.h index 31d606d5..b3e69065 100644 --- a/loader/src/include/linker_soinfo.h +++ b/loader/src/include/linker_soinfo.h @@ -375,6 +375,8 @@ struct soinfo { // ElfW(Addr) compat_relro_size_ = 0; public: + static constexpr size_t get_base_offset() { return offsetof(soinfo, base); } + static constexpr size_t get_size_offset() { return offsetof(soinfo, size); } static constexpr size_t get_next_offset() { return offsetof(soinfo, next); } diff --git a/loader/src/include/solist.hpp b/loader/src/include/solist.hpp index cc26e7bd..8cdf8f71 100644 --- a/loader/src/include/solist.hpp +++ b/loader/src/include/solist.hpp @@ -8,6 +8,7 @@ namespace Linker { class SoInfoWrapper { public: + inline static size_t field_base_offset = soinfo::get_base_offset(); inline static size_t field_size_offset = soinfo::get_size_offset(); inline static size_t field_next_offset = soinfo::get_next_offset(); inline static size_t field_constructor_called_offset = soinfo::get_constructors_called_offset(); @@ -17,6 +18,11 @@ class SoInfoWrapper { inline static void (*soinfo_free)(SoInfoWrapper *) = nullptr; inline static void (*soinfo_unload)(SoInfoWrapper *) = nullptr; + inline uintptr_t getBase() { + return *reinterpret_cast(reinterpret_cast(this) + + field_base_offset); + } + inline size_t getSize() { return *reinterpret_cast(reinterpret_cast(this) + field_size_offset); } @@ -115,7 +121,10 @@ const size_t llvm_suffix_length = 25; bool initialize(); bool findHeuristicOffsets(std::string linker_name, SoInfoWrapper *vdso); -bool dropSoPath(const char *target_pathn, bool unload); +bool dropSoPath(const char *target_pathn, bool unload, uintptr_t *out_base = nullptr, + size_t *out_size = nullptr); + +void dumpSoPath(const char *needle, const char *when); void resetCounters(size_t load, size_t unload); } // namespace Linker diff --git a/loader/src/injector/clean.cpp b/loader/src/injector/clean.cpp index 05db7889..bc265e3d 100644 --- a/loader/src/injector/clean.cpp +++ b/loader/src/injector/clean.cpp @@ -1,8 +1,16 @@ #include #include +#include #include +#ifndef PR_SET_VMA +#define PR_SET_VMA 0x53564d41 +#endif +#ifndef PR_SET_VMA_ANON_NAME +#define PR_SET_VMA_ANON_NAME 0 +#endif + #include "atexit.hpp" #include "fossil.hpp" #include "logging.hpp" @@ -18,9 +26,9 @@ void clean_libc_trace() { } void clean_linker_trace(const char *path, size_t loaded_modules, size_t unloaded_modules, - bool unload_soinfo) { + bool unload_soinfo, uintptr_t *out_base, size_t *out_size) { LOGV("cleaning linker trace for path %s", path); - Linker::dropSoPath(path, unload_soinfo); + Linker::dropSoPath(path, unload_soinfo, out_base, out_size); if (unload_soinfo) { Linker::resetCounters(loaded_modules, loaded_modules); @@ -60,6 +68,7 @@ void spoof_virtual_maps(const char *path, bool clear_write_permission) { munmap(copy, size); // Restore the original permissions mprotect(addr, size, map.perms); + prctl(PR_SET_VMA, PR_SET_VMA_ANON_NAME, addr, size, "dalvik-DEX data"); } if (clear_write_permission && map.path.size() > 0 && diff --git a/loader/src/injector/hook.cpp b/loader/src/injector/hook.cpp index 1e9a8676..713b0dec 100644 --- a/loader/src/injector/hook.cpp +++ b/loader/src/injector/hook.cpp @@ -427,7 +427,24 @@ void HookContext::restore_zygote_hook(JNIEnv *env) { void hook_entry(void *start_addr, size_t block_size) { g_hook = new HookContext(start_addr, block_size); g_hook->hook_plt(); - clean_linker_trace(zygiskd::GetTmpPath().data(), 1, 0, true); + + uintptr_t reserved_base = 0; + size_t reserved_size = 0; + clean_linker_trace(zygiskd::GetTmpPath().data(), 1, 0, true, &reserved_base, &reserved_size); + + // The ptracer sums only path-matched mappings, missing the anonymous .bss zeromap at + // the top of the reservation. soinfo::size is the real figure; trust it only when the + // base agrees, so a mismatch keeps the old behaviour. + if (reserved_base == reinterpret_cast(start_addr) && reserved_size > block_size) { + LOGV("linker reserved %zu bytes for us, %zu more than the ptracer measured; unmapping " + "the whole reservation", + reserved_size, reserved_size - block_size); + g_hook->block_size = reserved_size; + } else if (reserved_base != reinterpret_cast(start_addr)) { + LOGW("linker reservation base %p does not match the injected range at %p; keeping the " + "ptracer's size %zu", + (void *) reserved_base, start_addr, block_size); + } } void hookJniNativeMethods(JNIEnv *env, const char *clz, JNINativeMethod *methods, int numMethods) { diff --git a/loader/src/injector/module.cpp b/loader/src/injector/module.cpp index bc3a87e9..bb3ad5a5 100644 --- a/loader/src/injector/module.cpp +++ b/loader/src/injector/module.cpp @@ -16,6 +16,7 @@ #include "dl.hpp" #include "files.hpp" #include "logging.hpp" +#include "solist.hpp" #include "misc.hpp" #include "zygisk.hpp" @@ -318,13 +319,19 @@ void ZygiskContext::run_modules_post() { } else if (flags & SERVER_FORK_AND_SPECIALIZE) { m.postServerSpecialize(args.server); } - if (m.tryUnload()) modules_unloaded++; + } + // Reverse: the allocator is LIFO, so releasing back to front hands the blocks back + // in ascending order and leaves the allocation sequence monotonic. + for (auto it = modules.rbegin(); it != modules.rend(); ++it) { + if (it->tryUnload()) modules_unloaded++; } if (modules.size() > 0) { LOGV("modules unloaded: %zu/%zu", modules_unloaded, modules.size()); if (modules.size() == modules_unloaded) clean_libc_trace(); - clean_linker_trace("jit-cache-zygisk", modules.size(), modules_unloaded, true); + // "zygisk-module" is the memfd name (zygiskd.rs:264); b4d31ab fixed the same + // stale needle in spoof_virtual_maps but missed this call site. + clean_linker_trace("zygisk-module", modules.size(), modules_unloaded, true); g_hook->should_spoof_maps = (flags & APP_SPECIALIZE) && (modules.size() - modules_unloaded) > 0; } diff --git a/loader/src/injector/solist.cpp b/loader/src/injector/solist.cpp index 09801ef3..14d1cb1f 100644 --- a/loader/src/injector/solist.cpp +++ b/loader/src/injector/solist.cpp @@ -1,5 +1,8 @@ #include "solist.hpp" +#include +#include + #include "logging.hpp" namespace Linker { @@ -23,26 +26,14 @@ bool initialize() { if (soinfo_unload_name.empty()) return false; LOGV("found symbol name %s", soinfo_unload_name.data()); - char llvm_sufix[llvm_suffix_length + 1]; - - if (somain_sym_name.length() != strlen("__dl__ZL6somain")) { - strncpy(llvm_sufix, somain_sym_name.data() + strlen("__dl__ZL6somain"), sizeof(llvm_sufix)); - } else { - llvm_sufix[0] = '\0'; - } - - char solinker_sym_name[sizeof("__dl__ZL8solinker") + sizeof(llvm_sufix)]; - snprintf(solinker_sym_name, sizeof(solinker_sym_name), "__dl__ZL8solinker%s", llvm_sufix); + // The .llvm. suffix has no fixed width; Android 17 ships a 26-character one. + const std::string llvm_sufix(somain_sym_name.substr(strlen("__dl__ZL6somain"))); + const std::string solinker_sym_name = "__dl__ZL8solinker" + llvm_sufix; // for SDK < 36 (Android 16), the linker binary is loaded with name solist - char solist_sym_name[sizeof("__dl__ZL6solist") + sizeof(llvm_sufix)]; - snprintf(solist_sym_name, sizeof(solist_sym_name), "__dl__ZL6solist%s", llvm_sufix); - - char sonext_sym_name[sizeof("__dl__ZL6sonext") + sizeof(llvm_sufix)]; - snprintf(sonext_sym_name, sizeof(sonext_sym_name), "__dl__ZL6sonext%s", llvm_sufix); - - char vdso_sym_name[sizeof("__dl__ZL4vdso") + sizeof(llvm_sufix)]; - snprintf(vdso_sym_name, sizeof(vdso_sym_name), "__dl__ZL4vdso%s", llvm_sufix); + const std::string solist_sym_name = "__dl__ZL6solist" + llvm_sufix; + const std::string sonext_sym_name = "__dl__ZL6sonext" + llvm_sufix; + const std::string vdso_sym_name = "__dl__ZL4vdso" + llvm_sufix; solinker = ElfParser::resolveSymbolPointer(linker, solinker_sym_name); if (solinker == nullptr) { @@ -106,8 +97,11 @@ bool findHeuristicOffsets(std::string linker_name, SoInfoWrapper *vdso) { if (!size_field_found) { if (size_of_somain < size_maximal && size_of_somain > size_minimal) { SoInfoWrapper::field_size_offset = i * sizeof(void *); - LOGV("heuristic field_size_offset is %zu * %zu = %p", i, sizeof(void *), - reinterpret_cast(SoInfoWrapper::field_size_offset)); + SoInfoWrapper::field_base_offset = + SoInfoWrapper::field_size_offset - sizeof(ElfW(Addr)); + LOGV("heuristic field_size_offset is %zu * %zu = %p (base at %p)", i, + sizeof(void *), reinterpret_cast(SoInfoWrapper::field_size_offset), + reinterpret_cast(SoInfoWrapper::field_base_offset)); size_field_found = true; continue; } @@ -167,31 +161,70 @@ bool findHeuristicOffsets(std::string linker_name, SoInfoWrapper *vdso) { return size_field_found && next_field_found && constructor_called_field_found; } -bool dropSoPath(const char *target_path, bool unload) { +void dumpSoPath(const char *needle, const char *when) { + if (solinker == nullptr && !initialize()) return; + size_t n = 0; + for (auto *iter = solinker; iter; iter = iter->getNext()) { + if (iter->getPath() && strstr(iter->getPath(), needle)) { + LOGV("[%s] solist record %p size %zu for %s", when, (void *) iter, iter->getSize(), + iter->getPath()); + n++; + } + } + LOGV("[%s] %zu record(s) matching \"%s\"", when, n, needle); +} + +bool dropSoPath(const char *target_path, bool unload, uintptr_t *out_base, size_t *out_size) { bool path_found = false; if (solinker == nullptr && !initialize()) { LOGE("failed to initialize solist before dropping paths"); return path_found; } + + // soinfo_free zeroes the block, so walking the list while dropping stops after the + // first hit. Release highest block first: the allocator is LIFO, so that hands them + // back in ascending order and the allocation sequence stays monotonic. + std::vector targets; for (auto *iter = solinker; iter; iter = iter->getNext()) { - if (iter->getPath() && strstr(iter->getPath(), target_path)) { - Linker::ProtectedDataGuard guard; - auto size = iter->getSize(); - LOGV("dropping solist record for %s [size %zu, constructor_called: %d]", - iter->getPath(), size, iter->getConstructorCalled()); - if (size > 0) { - iter->setSize(0); - if (unload) { - iter->setConstructorCalled(false); - SoInfoWrapper::soinfo_unload(iter); - iter->setConstructorCalled(true); - } else { - SoInfoWrapper::soinfo_free(iter); - iter->setSize(size); - } - path_found = true; + if (iter->getPath() && strstr(iter->getPath(), target_path) && iter->getSize() > 0) { + targets.push_back(iter); + } + } + std::sort(targets.begin(), targets.end(), [](SoInfoWrapper *a, SoInfoWrapper *b) { + return reinterpret_cast(a) > reinterpret_cast(b); + }); + + for (auto *target : targets) { + // A previous unload may have taken this one with it; a double unload aborts. + bool still_linked = false; + for (auto *iter = solinker; iter; iter = iter->getNext()) { + if (iter == target) { + still_linked = true; + break; } } + if (!still_linked) { + LOGV("solist record %p went away with an earlier unload", (void *) target); + continue; + } + + Linker::ProtectedDataGuard guard; + auto size = target->getSize(); + if (size == 0) continue; + LOGV("dropping solist record %p for %s [size %zu, constructor_called: %d]", + (void *) target, target->getPath(), size, target->getConstructorCalled()); + if (out_base != nullptr && *out_base == 0) *out_base = target->getBase(); + if (out_size != nullptr && *out_size == 0) *out_size = size; + target->setSize(0); + if (unload) { + target->setConstructorCalled(false); + SoInfoWrapper::soinfo_unload(target); + target->setConstructorCalled(true); + } else { + SoInfoWrapper::soinfo_free(target); + target->setSize(size); + } + path_found = true; } return path_found; } diff --git a/loader/src/injector/zygisk.hpp b/loader/src/injector/zygisk.hpp index 78ba69ca..cd81fa87 100644 --- a/loader/src/injector/zygisk.hpp +++ b/loader/src/injector/zygisk.hpp @@ -25,7 +25,8 @@ void hookJniNativeMethods(JNIEnv *env, const char *clz, JNINativeMethod *methods void clean_libc_trace(); void clean_linker_trace(const char *path, size_t loaded_modules, size_t unloaded_modules, - bool unload_soinfo); + bool unload_soinfo, uintptr_t *out_base = nullptr, + size_t *out_size = nullptr); void spoof_virtual_maps(const char *path, bool clear_write_permission); diff --git a/loader/src/ptracer/ptracer.cpp b/loader/src/ptracer/ptracer.cpp index 3be6893b..166ff937 100644 --- a/loader/src/ptracer/ptracer.cpp +++ b/loader/src/ptracer/ptracer.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -273,13 +274,30 @@ bool inject_on_main(int pid, const char *lib_path) { map = MapInfo::Scan(std::to_string(pid)); void *start_addr = nullptr; size_t block_size = 0; + // Use the pathname only to identify the library once, then collect its mappings by + // device and inode. A substring test is not a safe key for the collection pass: a + // "(deleted)" suffix on a replaced file, or any second mapping whose path merely + // contains the same basename, would be folded into the range we later unmap. + // dev+inode names the file itself, and it also excludes anonymous mappings, whose + // inode is zero. + dev_t lib_dev = 0; + ino_t lib_inode = 0; for (const auto &info : map) { - if (info.path.find("libzygisk.so") != std::string::npos) { + if (info.inode != 0 && info.path.find("libzygisk.so") != std::string::npos) { + lib_dev = info.dev; + lib_inode = info.inode; + break; + } + } + if (lib_inode != 0) { + for (const auto &info : map) { + if (info.dev != lib_dev || info.inode != lib_inode) continue; if (start_addr == nullptr) start_addr = (void *) info.start; block_size += (info.end - info.start); } } - LOGV("found injected library mapped from %p with total size %zu", start_addr, block_size); + LOGV("found injected library mapped from %p with total size %zu [dev %u:%u inode %lu]", + start_addr, block_size, major(lib_dev), minor(lib_dev), (unsigned long) lib_inode); // Remotely call our entry(start_addr, block_size, path) function LOGI("calling the injector's entry function to initialize NeoZygisk");