# How Zygisk Injects Code Into Every Android Application Process via Zygote

> Discover how Zygisk injects code into every Android app. Learn about PLT hooking, Zygote initialization detection, and custom method replacement for seamless module loading. Understand the underlying mechanism of Zygisk.

- Repository: [John Wu/Magisk](https://github.com/topjohnwu/Magisk)
- Tags: internals
- Published: 2026-03-05

---

**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`](https://github.com/topjohnwu/Magisk/blob/main/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`](https://github.com/topjohnwu/Magisk/blob/main/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.

```cpp
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`](https://github.com/topjohnwu/Magisk/blob/main/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.

```cpp
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.

```cpp
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`](https://github.com/topjohnwu/Magisk/blob/main/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.

```cpp
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.