How Zygisk Companion Processes Enable Secure Module Injection in Magisk
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 symbolzygisk_companion_entryand maintains function pointers for each loaded module in themodulesvector. - 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.
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:
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). 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.
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:
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 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 (lines 9-10). This macro expands to export the symbol zygisk_companion_entry that the daemon resolves during initialization.
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:
#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:
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:
magiskdsends module file descriptors to the target Zygisk process during initialization.- The Zygisk process loads the module through
zygisk_module_entry(native/src/core/zygisk/module.cpp, lines 60-64). - When privileged work is needed, the module calls
Api::connectCompanion()(native/src/core/zygisk/api.hpp, lines 64-66) to obtain a daemon socket. - The module transmits its module index and protocol data over the socket.
- The daemon receives the request, validates the module ID, and invokes the stored
zygisk_companion_entrypointer viaexec_companion_entry. - The module's companion code executes within
zygiskdwith 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_extand resolves thezygisk_companion_entrysymbol 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_COMPANIONmacro exports the required symbol that the daemon calls when handling privileged requests. - This split-architecture maintains security by isolating root-level operations in
zygiskdwhile 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, 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 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.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →