# How Zygisk Companion Processes Enable Secure Module Injection in Magisk

> Learn how Zygisk companion processes enable secure module injection in Magisk by splitting tasks between app and daemon processes for root operations via IPC.

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

---

**Zygisk companion processes allow Magisk modules to execute privileged root operations by splitting functionality between the unprivileged app process and a dedicated daemon that handles sensitive tasks through socket-based IPC.**

Zygisk companion processes form the backbone of privileged module injection in the Magisk framework. According to the topjohnwu/Magisk source code, this architecture isolates dangerous operations within a root daemon (`zygiskd`) while keeping the main injection logic inside the target application process. This separation ensures that modules can perform file system operations or other sensitive tasks without exposing the entire app process to elevated privileges.

## The Two-Process Architecture

Zygisk splits module injection work between two distinct processes to maintain security boundaries:

- **zygiskd (companion daemon)**: Runs as a privileged root daemon that loads compiled module libraries from file descriptors supplied by `magiskd`. It resolves the exported symbol `zygisk_companion_entry` and maintains function pointers for each loaded module in the `modules` vector.
- **zygisk (target app process)**: Loads the module library through the standard Zygisk hook path (`zygisk_module_entry`). When the module requires privileged work, it requests the daemon to execute the companion entry point.

## Loading the Companion Library

The daemon initialization begins when `zygisk_main` is invoked with the arguments `zygisk companion <socket>` in [`native/src/core/zygisk/entry.cpp`](https://github.com/topjohnwu/Magisk/blob/main/native/src/core/zygisk/entry.cpp).

The `zygiskd(int socket)` function receives a list of file descriptors pointing to module libraries via `recv_fds`. For each descriptor, it constructs an `android_dlextinfo` structure instructing `android_dlopen_ext` to load the library from the FD using the `/jit-cache` namespace:

```cpp
if (void *h = android_dlopen_ext("/jit-cache", RTLD_LAZY, &info)) {
    *(void **) &entry = dlsym(h, "zygisk_companion_entry");
}

```

After loading, the daemon resolves the `zygisk_companion_entry` symbol using `dlsym` and stores the resulting function pointer in the `modules` vector (lines 46-47 of [`entry.cpp`](https://github.com/topjohnwu/Magisk/blob/main/entry.cpp)). This pointer serves as the entry point for privileged module operations.

## Dispatching Companion Requests

When a module requires privileged assistance, it initiates communication through the API defined in [`native/src/core/zygisk/api.hpp`](https://github.com/topjohnwu/Magisk/blob/main/native/src/core/zygisk/api.hpp).

The module calls `Api::connectCompanion()` (lines 64-66), which returns a socket file descriptor connected directly to the daemon. The module writes its **module ID** over this socket, followed by any custom protocol data required for the operation.

The daemon's main loop receives the client socket through `recv_fd`, reads the module identifier using `read_int`, and dispatches the request:

```cpp
int module_id = read_int(client);
if (module_id >= 0 && module_id < modules.size() && modules[module_id]) {
    exec_companion_entry(client, modules[module_id]);
}

```

The `exec_companion_entry` function (declared in [`entry.cpp`](https://github.com/topjohnwu/Magisk/blob/main/entry.cpp) lines 13-14 as `extern "C"`) forwards the client socket directly to the module's companion entry function, allowing the privileged code to execute within the daemon context.

## Implementing the Module Side

Module authors implement companion functionality by defining a function that receives the daemon client socket and registering it with the `REGISTER_ZYGISK_COMPANION` macro defined in [`native/src/core/zygisk/api.hpp`](https://github.com/topjohnwu/Magisk/blob/main/native/src/core/zygisk/api.hpp) (lines 9-10). This macro expands to export the symbol `zygisk_companion_entry` that the daemon resolves during initialization.

```cpp
static void my_companion(int client) {
    // Example: request root file access via the daemon
    const char *cmd = "open /data/important.txt O_RDONLY";
    write(client, cmd, strlen(cmd));
    // read response, etc.
}
REGISTER_ZYGISK_COMPANION(my_companion);

```

In the app process, the module obtains the companion socket and triggers the privileged execution:

```cpp
#include <zygisk/api.hpp>
using namespace zygisk;

static void copy_file_companion(int client) {
    const char *msg = "/data/local/tmp/secret.txt /sdcard/secret_copy.txt";
    write(client, msg, strlen(msg));
    char reply[64];
    read(client, reply, sizeof(reply));
}

REGISTER_ZYGISK_COMPANION(copy_file_companion);

void install_init(zyp::Api *api) {
    int daemon_sock = api->connectCompanion();
    if (daemon_sock < 0) return;
    write_int(daemon_sock, 0);  // module index 0
    // Daemon invokes copy_file_companion(daemon_sock) as root
}

```

The companion implementation running in the daemon handles the privileged operation:

```cpp
static void my_companion(int client) {
    char buf[256];
    ssize_t len = read(client, buf, sizeof(buf) - 1);
    if (len <= 0) return;
    buf[len] = '\0';
    char src[128], dst[128];
    if (sscanf(buf, "%127s %127s", src, dst) != 2) return;
    if (copy_file(src, dst) == 0) {
        write(client, "OK", 2);
    } else {
        write(client, "FAIL", 4);
    }
}
REGISTER_ZYGISK_COMPANION(my_companion);

```

## Complete Injection Flow

The full Zygisk companion process lifecycle involves coordinated communication between `magiskd`, the companion daemon, and the target app process:

1. **`magiskd`** sends module file descriptors to the target Zygisk process during initialization.
2. The Zygisk process loads the module through `zygisk_module_entry` ([`native/src/core/zygisk/module.cpp`](https://github.com/topjohnwu/Magisk/blob/main/native/src/core/zygisk/module.cpp), lines 60-64).
3. When privileged work is needed, the module calls `Api::connectCompanion()` ([`native/src/core/zygisk/api.hpp`](https://github.com/topjohnwu/Magisk/blob/main/native/src/core/zygisk/api.hpp), lines 64-66) to obtain a daemon socket.
4. The module transmits its module index and protocol data over the socket.
5. The daemon receives the request, validates the module ID, and invokes the stored `zygisk_companion_entry` pointer via `exec_companion_entry`.
6. The module's companion code executes within `zygiskd` with root privileges, performs the operation, and returns results over the socket.

This architecture ensures that sensitive operations remain isolated within the daemon process while the injection mechanism stays resident in the app process.

## Summary

- **Zygisk companion processes** separate privileged operations from the app process by executing them in a dedicated root daemon.
- The daemon loads module libraries from file descriptors using `android_dlopen_ext` and resolves the `zygisk_companion_entry` symbol for each module.
- Modules communicate with the daemon via sockets obtained through `Api::connectCompanion()`, passing their module ID to trigger the companion entry.
- The `REGISTER_ZYGISK_COMPANION` macro exports the required symbol that the daemon calls when handling privileged requests.
- This split-architecture maintains security by isolating root-level operations in `zygiskd` while preserving normal Zygisk hook functionality in the target process.

## Frequently Asked Questions

### What is the purpose of Zygisk companion processes in Magisk?

Zygisk companion processes provide a secure mechanism for Magisk modules to execute privileged operations that require root access. By running these operations in a separate daemon process (`zygiskd`) rather than directly within the target app process, the architecture maintains security boundaries and prevents the injection of full root privileges into every app process.

### How does a module communicate with the Zygisk companion daemon?

A module communicates with the companion daemon through a Unix domain socket obtained via `Api::connectCompanion()`. The module writes its module ID to this socket, followed by any custom protocol data. The daemon reads the module ID, looks up the corresponding function pointer in its `modules` vector, and executes the `zygisk_companion_entry` function with the client socket as an argument.

### What symbol must a module export to use companion processes?

A module must export the symbol `zygisk_companion_entry` to use companion processes. Module authors typically use the `REGISTER_ZYGISK_COMPANION(function_name)` macro defined in [`native/src/core/zygisk/api.hpp`](https://github.com/topjohnwu/Magisk/blob/main/native/src/core/zygisk/api.hpp), which automatically creates the properly named exported symbol pointing to the author's companion function.

### Where is the companion daemon initialized in the Magisk source code?

The companion daemon initializes in [`native/src/core/zygisk/entry.cpp`](https://github.com/topjohnwu/Magisk/blob/main/native/src/core/zygisk/entry.cpp) when `zygisk_main` receives the command `zygisk companion <socket>`. The `zygiskd` function handles the daemon lifecycle, including receiving module file descriptors via `recv_fds`, loading libraries with `android_dlopen_ext`, and entering the main request dispatch loop.