Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions loader/src/include/linker_soinfo.h
Original file line number Diff line number Diff line change
Expand Up @@ -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); }
Expand Down
11 changes: 10 additions & 1 deletion loader/src/include/solist.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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<uintptr_t *>(reinterpret_cast<uintptr_t>(this) +
field_base_offset);
}

inline size_t getSize() {
return *reinterpret_cast<size_t *>(reinterpret_cast<uintptr_t>(this) + field_size_offset);
}
Expand Down Expand Up @@ -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
13 changes: 11 additions & 2 deletions loader/src/injector/clean.cpp
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
#include <linux/mman.h>
#include <sys/mman.h>
#include <sys/prctl.h>

#include <lsplt.hpp>

#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"
Expand All @@ -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);
Expand Down Expand Up @@ -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 &&
Expand Down
19 changes: 18 additions & 1 deletion loader/src/injector/hook.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<uintptr_t>(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<uintptr_t>(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) {
Expand Down
11 changes: 9 additions & 2 deletions loader/src/injector/module.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
#include "dl.hpp"
#include "files.hpp"
#include "logging.hpp"
#include "solist.hpp"
#include "misc.hpp"
#include "zygisk.hpp"

Expand Down Expand Up @@ -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;
}
Expand Down
107 changes: 70 additions & 37 deletions loader/src/injector/solist.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
#include "solist.hpp"

#include <algorithm>
#include <vector>

#include "logging.hpp"

namespace Linker {
Expand All @@ -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.<hash> 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<SoInfoWrapper>(linker, solinker_sym_name);
if (solinker == nullptr) {
Expand Down Expand Up @@ -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<void *>(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<void *>(SoInfoWrapper::field_size_offset),
reinterpret_cast<void *>(SoInfoWrapper::field_base_offset));
size_field_found = true;
continue;
}
Expand Down Expand Up @@ -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<SoInfoWrapper *> 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<uintptr_t>(a) > reinterpret_cast<uintptr_t>(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;
}
Expand Down
3 changes: 2 additions & 1 deletion loader/src/injector/zygisk.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
22 changes: 20 additions & 2 deletions loader/src/ptracer/ptracer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include <sys/auxv.h>
#include <sys/mman.h>
#include <sys/ptrace.h>
#include <sys/sysmacros.h>
#include <sys/system_properties.h>
#include <sys/uio.h>
#include <sys/wait.h>
Expand Down Expand Up @@ -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");
Expand Down
Loading