How Magisk Daemon Communicates with the App via Unix Socket IPC
The Magisk daemon (magiskd) creates a Unix domain socket at MAGISK_TMP/MAGISK_SOCKET that accepts binary-encoded requests from the Android app, validates peer credentials, and dispatches commands through synchronous, asynchronous, or boot-stage handler tiers.
The topjohnwu/Magisk repository implements a custom, permission-checked IPC channel to bridge the root-level daemon with the unprivileged Magisk app and native clients. This system relies on a private Unix socket residing on the Magisk temporary file system rather than Android Binder, enabling structured binary messaging and file descriptor passing between processes.
Socket Creation and Daemon Startup
When the daemon initializes via daemon_entry in native/src/core/daemon.rs, it constructs the socket path by joining get_magisk_tmp() with the MAIN_SOCKET constant (defined as MAGISK_SOCKET in native/include/consts.rs). The daemon binds a UnixListener to this path, sets world-writable permissions (0o666), and applies the MAGISK_FILE_CON SELinux context to ensure the socket is accessible to the app.
let sock_path = cstr::buf::new::<64>()
.join_path(get_magisk_tmp())
.join_path(MAIN_SOCKET);
let sock = UnixListener::bind(&sock_path)?;
sock_path.follow_link().chmod(0o666)?;
sock_path.set_secontext(cstr!(MAGISK_FILE_CON))?;
The daemon then enters a loop accepting connections and spawning threads to handle incoming clients.
Client Connection Flow
Any client—including the Magisk app via JNI—initiates communication through connect_daemon, exposed in the CXX bridge within native/src/core/lib.rs. This function attempts to connect to the existing socket; if the daemon is not running and the caller is root (getuid().is_root()), it can optionally fork and launch the daemon automatically.
pub fn connect_daemon(code: RequestCode, create: bool) -> LoggedResult<UnixStream> {
let sock_path = …; // same path as daemon
match UnixStream::connect(&sock_path) {
Ok(socket) => send_request(code, socket), // daemon already running
Err(_) => { // daemon not running
if create && getuid().is_root() { … fork & daemon_entry() … }
}
}
}
Once connected, the client receives a raw file descriptor that is wrapped into a UnixStream for subsequent I/O operations.
Binary Message Framing and Protocol
The IPC protocol uses a simple binary framing mechanism. Clients write a 32-bit RequestCode (defined in the CXX enum in native/src/core/lib.rs) to the socket, and the daemon responds with a 32-bit RespondCode indicating success or failure (e.g., OK, ROOT_REQUIRED, ACCESS_DENIED).
// Client side
socket.write_pod(&code.repr)?;
let mut res = -1;
socket.read_pod(&mut res)?;
// Server side (daemon.rs)
let mut code = -1;
client.read_pod(&mut code)?;
let code = RequestCode { repr: code };
// permission checks …
client.write_pod(&RespondCode::OK.repr)?;
The write_pod and read_pod methods facilitate fixed-size primitive data exchange, while variable-length payloads use the Encodable and Decodable traits defined in native/src/core/socket.rs.
Request Dispatch and Permission Validation
Upon receiving a request, the daemon’s handle_requests function (in native/src/core/daemon.rs) inspects the peer’s Unix credentials (UCred) to validate whether the caller is root, the Zygote process, or an explicitly allowed application. After validation, the daemon categorizes the request code into one of three handler tiers:
- Synchronous handling – Codes below
_SYNC_BARRIER_are processed immediately in the same thread viahandle_request_sync. - Asynchronous handling – Codes between
_SYNC_BARRIER_and_STAGE_BARRIER_are dispatched to a thread pool throughhandle_request_async. - Boot-stage handling – Codes above
_STAGE_BARRIER_are routed toboot_stage_handlerfor early-boot operations.
This tiered approach prevents blocking the main accept loop during long-running operations like superuser request processing or Zygisk module initialization.
Structured Data and File Descriptor Passing
For complex interactions, the native/src/core/socket.rs module provides a custom binary codec. Types implement the Encodable and Decodable traits to serialize POD values, strings, and vectors over the UnixStream. This enables structured data transfer for requests like SUPERUSER, where the daemon receives a SuRequest struct:
fn handle_request_async(&self, mut client: UnixStream, code: RequestCode, cred: UCred) {
match code {
RequestCode::SUPERUSER => {
let su_req: SuRequest = client.read_decodable().unwrap();
self.su_daemon_handler(client, cred);
}
// … other request handling …
}
}
When a request requires passing an open file descriptor—such as a PTY master for shell sessions—the daemon uses send_fd and recv_fd helpers that transmit the FD using SCM_RIGHTS ancillary data over the socket.
let pty_fd = open_pty()?;
send_fd(client.as_raw_fd(), pty_fd); // Transfers FD using SCM_RIGHTS
Summary
- The daemon binds a
UnixListeneratMAGISK_TMP/MAIN_SOCKETwith0666permissions and theMAGISK_FILE_CONSELinux context. - Clients invoke
connect_daemon()fromnative/src/core/lib.rsto establish aUnixStream, spawning the daemon if necessary. - Communication uses 32-bit
RequestCodeandRespondCodeintegers for simple command/response pairs. - The daemon validates peer credentials and dispatches requests through synchronous, asynchronous, or boot-stage handlers based on barrier constants.
- Complex payloads utilize
Encodable/Decodabletraits innative/src/core/socket.rs. - File descriptors are transferred via
SO_PASSCRED-style ancillary data usingsend_fdandrecv_fd.
Frequently Asked Questions
How does the Magisk app establish a socket connection from Java or Kotlin?
The Android app connects through a JNI bridge that invokes connect_daemon_for_cxx defined in native/src/core/lib.rs. This Rust function returns a raw file descriptor as an int, which the Java wrapper converts into a ParcelFileDescriptor or uses directly via native code to exchange RequestCode values and read daemon responses.
What security mechanisms restrict access to the Magisk socket?
The daemon enforces three layers of security: Unix credentials (UCred) retrieved via SO_PEERCRED validate the UID/PID of the caller; explicit permission checks in handle_requests filter for root, Zygote, or allowlisted apps; and the socket file itself carries the MAGISK_FILE_CON SELinux context, ensuring only properly labeled processes can access it even with 0666 permissions.
Why does Magisk use Unix sockets instead of Android Binder for IPC?
Unix domain sockets provide a simpler, lower-level abstraction that works reliably in early boot stages before the full Android framework is available, and they natively support passing arbitrary file descriptors (like PTYs or mount points) via SCM_RIGHTS. This avoids the complexity and dependency overhead of Binder while maintaining compatibility with both native code and JNI layers.
How are variable-length data structures serialized over the socket?
The native/src/core/socket.rs module defines Encodable and Decodable traits that implement a custom binary protocol. These traits handle primitive types, length-prefixed strings, and vectors by writing raw bytes to the UnixStream, allowing structured data like SuRequest objects to be transmitted seamlessly alongside simple integer response codes.
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 →