How Zygisk Injects Code Into Every Android Application Process via Zygote

Zygisk injects code into every Android app process by hooking the Zygote process's PLT entries, detecting Zygote initialization via a strdup intercept, and replacing the native JNI methods nativeForkAndSpecialize and nativeSpecializeAppProcess with custom wrappers that load Zygisk modules before the app's own code executes.

The Magisk framework's Zygisk component enables powerful runtime modifications by intercepting the Android Zygote process—the parent of all application processes. According to the topjohnwu/Magisk source code, Zygisk achieves universal code injection by manipulating Procedure Linkage Table (PLT) hooks and JNI method registrations within the Zygote process, ensuring that every forked app loads designated modules during its initialization sequence.

Hijacking the Native Bridge Entry Point

Zygisk establishes its presence early by masquerading as the Android native bridge. When the system loads libnativebridge.so, Zygisk intercepts this operation to inject libzygisk.so instead.

In native/src/core/zygisk/hook.cpp, the HookContext::post_native_bridge_load() function executes immediately after the native bridge is mapped. By analyzing the backtrace to locate android::LoadNativeBridge and its NativeBridgeRuntimeCallbacks, Zygisk gains control before the Zygote process fully initializes. This early execution is critical because it allows Zygisk to install hooks before the runtime completes its setup.

Hooking PLT Functions in System Libraries

Once loaded, Zygisk scans memory maps for libandroid_runtime.so and libnativebridge.so to install PLT hooks. The HookContext::hook_plt() method, located at lines 81-119 of hook.cpp, identifies these libraries by their device and inode numbers, then redirects several key functions:

  • dlclose and android_log_close for cleanup interception
  • fork and unshare for process creation monitoring
  • selinux_android_setcontext for security context manipulation
  • strdup for Zygote initialization detection

The hooks are registered using lsplt::RegisterHook and committed via lsplt::CommitHook(), effectively replacing the original function pointers in the global offset table with Zygisk's implementations.

void HookContext::hook_plt() {
    // Find the library maps
    for (auto &map : lsplt::MapInfo::Scan()) {
        if (map.path.ends_with("/libandroid_runtime.so")) {
            android_runtime_dev = map.dev;
            android_runtime_inode = map.inode;
        } else if (map.path.ends_with("/libnativebridge.so")) {
            native_bridge_dev = map.dev;
            native_bridge_inode = map.inode;
        }
    }
    // Replace selected symbols with our implementations
    PLT_HOOK_REGISTER(native_bridge_dev, native_bridge_inode, dlclose);
    PLT_HOOK_REGISTER(android_runtime_dev, android_runtime_inode, fork);
    PLT_HOOK_REGISTER(android_runtime_dev, android_runtime_inode, strdup);
    // …more hooks…
    lsplt::CommitHook();   // apply the PLT patch
}

Detecting Zygote Initialization via String Interception

Zygisk identifies the exact moment Zygote starts by monitoring the strdup function. When the Android runtime initializes, it duplicates the string "ZygoteInit" via strdup. Zygisk's hooked version of this function checks for this specific string literal.

In hook.cpp (lines 44-48), the new_strdup function compares incoming strings against kZygoteInit. Upon detection, it triggers HookContext::hook_zygote_jni() to begin the JNI method replacement phase before returning control to the original strdup implementation.

static char *new_strdup(const char *str) {
    if (strcmp(kZygoteInit, str) == 0) {   // Zygote is initializing
        g_hook->hook_zygote_jni();          // replace Zygote JNI methods
    }
    return old_strdup(str);
}

Replacing Zygote JNI Methods

After detecting Zygote initialization, Zygisk replaces the native methods responsible for process specialization. The HookContext::hook_zygote_jni() function obtains the current JavaVM and JNIEnv, locates the com/android/internal/os/Zygote class, and swaps the native implementations of three critical methods:

  1. nativeForkAndSpecialize → replaced with fork_app_methods
  2. nativeSpecializeAppProcess → replaced with specialize_app_methods
  3. nativeForkSystemServer → replaced with fork_server_methods

These wrapper arrays, defined in the JNI hooks implementation, intercept calls to the original Dalvik/ART runtime functions. When Zygote forks a new application process, control flows through Zygisk's wrappers first, enabling module injection before the app begins execution.

bool HookContext::hook_zygote_jni() {
    // …obtain JNIEnv & JavaVM…
    jclass zygote = env->FindClass("com/android/internal/os/Zygote");
    // Replace the native method with our wrapper
    hook_jni_methods(env, zygote, fork_app_methods);
    // similarly replace specialize_app_methods and fork_server_methods
}

Loading Modules into Child Processes

When the hooked JNI methods detect a fork event, they invoke ZygiskContext callbacks such as nativeForkAndSpecialize_* and nativeSpecializeAppProcess_*. These handlers establish a socket connection to magiskd to query which Zygisk modules should load for the target UID.

The ZygiskContext::run_modules_pre() function in native/src/core/zygisk/module.cpp (lines 48-68) receives file descriptors for module shared objects via this socket. It loads each module using android_dlopen_ext with the ANDROID_DLEXT_USE_LIBRARY_FD flag, mapping the libraries from memory rather than disk paths. After loading, it resolves the zygisk_module_entry symbol and executes the module's preAppSpecialize hook before allowing the original specialization logic to proceed.

void ZygiskContext::run_modules_pre(rust::Vec<int> &fds) {
    for (int fd : fds) {
        android_dlextinfo info{
            .flags = ANDROID_DLEXT_USE_LIBRARY_FD,
            .library_fd = fd,
        };
        void *h = android_dlopen_ext("/jit-cache", RTLD_LAZY, &info);
        if (h && (void *e = dlsym(h, "zygisk_module_entry")))
            modules.emplace_back(i, h, e);
    }
    // call pre‑specialize hooks
    for (auto &m : modules) m.preAppSpecialize(args.app);
}

Post-Specialization Execution and Cleanup

After the original specialization completes, Zygisk executes postAppSpecialize hooks, allowing modules to modify the fully initialized process environment. Once the application is running, Zygisk restores the original PLT hooks via HookContext::restore_plt_hook() to minimize detection surface.

For self-unloading, Zygisk exploits a pthread_attr_destroy hook that safely calls dlclose on its own handle when the process exits, ensuring no memory leaks or lingering traces remain in the address space.

Summary

  • Early Injection: Zygisk loads via the native bridge mechanism before Zygote initializes, establishing control in post_native_bridge_load().
  • PLT Hooking: The system scans libandroid_runtime.so and installs hooks on critical functions including fork, strdup, and selinux_android_setcontext using the lsplt library.
  • Zygote Detection: Intercepting strdup for the "ZygoteInit" string triggers the JNI replacement phase.
  • JNI Replacement: Native methods nativeForkAndSpecialize and nativeSpecializeAppProcess are redirected to Zygisk wrappers that intercept every app spawn.
  • Module Loading: Child processes receive module file descriptors from magiskd, load them via android_dlopen_ext, and execute preAppSpecialize hooks before the app starts.
  • Stealth Cleanup: PLT hooks are restored after use, and the library unloads itself via a pthread_attr_destroy callback.

Frequently Asked Questions

What is the Android Zygote process and why does Zygisk target it?

The Zygote process is Android's template process that forks to create all application processes. By targeting Zygote, Zygisk ensures that every application inherits its injected code automatically, eliminating the need to modify each app individually. This central interception point guarantees universal coverage across the entire Android user space.

How does Zygisk avoid interfering with system stability when hooking low-level functions?

Zygisk employs precise PLT hooking rather than inline patching, which preserves the original function pointers for delegation. After intercepting calls in its wrappers, Zygisk calls the original implementations to maintain normal Android runtime behavior. The cleanup phase restores original PLT entries after initialization completes, minimizing the persistent modification footprint.

What is the difference between preAppSpecialize and postAppSpecialize hooks?

preAppSpecialize executes before the Android runtime completes app process specialization, allowing modules to modify UID, GID, namespace, and SELinux contexts. postAppSpecialize runs after specialization finishes, when the process environment is fully established but before the application code begins execution. Modules use the former to alter process creation parameters and the latter to patch runtime APIs or hide modifications from app detection mechanisms.

Can Zygisk modules target specific applications rather than all processes?

Yes. During the fork interception phase, Zygisk queries magiskd with the target UID to determine which modules should load. The daemon maintains a module list per UID, allowing selective injection. Modules themselves can also check the package name within their preAppSpecialize callback and abort loading if the target does not match their intended scope.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →