Before you start#
Required: Android’s Security Model, Verified Boot and Encryption, Rooting Methods, and SELinux Policy Lab. Magisk is a third-party open-source Android customization project, not an AOSP component or a Google-supported product. This chapter explains pinned v28.1 source, not current release behavior or an installation recipe. No device modification is required.
Learning goals#
Trace the roles of magiskinit, Android init, magiskd, a su client, manager-mediated policy, and the forked command. Distinguish UID, capabilities, SELinux context/policy, and mount namespace. Analyze which authorization lifecycle details are shown by the pinned source and which remain unknown without more source or runtime testing.
Systemless: what it means
“Systemless” describes a way to present runtime changes—often through boot setup and mount overlays—without directly rewriting the original system filesystem in the traditional manner. It does not mean no boot image changed, no persistent state exists, no code runs with powerful access, or Verified Boot sees the original system. MagiskSU (privileged execution), modules (customization), MagiskBoot (image tooling), and optional Zygisk (code in app processes) are distinct features. Do not infer that enabling one necessarily enables all others.
The v28.1 source path
All implementation details in this section are pinned to Magisk tag v28.1, commit 218327f92b572c183718e0fe722c2babf293535f.
Early boot returns to Android init
- Boot-selected MagiskInit path
Choose setup according to the actual ramdisk/root/stage arrangement.
- Layout-dependent setup
Early userspace preparation is not one universal mount recipe.
- exec_init()
Hand off to the original Android init path rather than replacing all Android service management.
In pinned native/src/init/init.cpp, the PID-1 path constructs MagiskInit, chooses setup according to boot layout/stage, then calls exec_init() to hand off to Android init. Separate paths account for first/second-stage init, system-as-root, recovery, and rootfs arrangements. This is not a replacement for Android init or a universal sequence of mount calls.
Daemon request gate
Pinned native/src/core/daemon.cpp sets the daemon’s SELinux context, creates a local Unix socket, reads peer credentials, and dispatches request codes. For a superuser request it calls su_daemon_handler() with peer credentials. Socket access is not itself permission to perform every daemon operation: the source has request-specific checks, including UID/context/executable checks on the general request path.
UID policy and manager mediation
- su clientmagiskd / policy
request; daemon obtains peer UID / PID
- magiskd / policyManager
if QUERY: request a policy decision
- Managermagiskd / policy
Return. decision or failure → deny
- magiskd / policyCommand child
only if authorized: fork and set up identity
- Command childmagiskd / policy
Return. child completion status
- magiskd / policysu client
Return. report result
Time runs downward. Dashed arrows show returns.
Read the text version
- su client → magiskd / policy: request; daemon obtains peer UID / PID
- magiskd / policy → Manager: if QUERY: request a policy decision
- Manager → magiskd / policy (return): decision or failure → deny
- magiskd / policy → Command child: only if authorized: fork and set up identity
- Command child → magiskd / policy (return): child completion status
- magiskd / policy → su client (return): report result
Pinned native/src/core/su/su_daemon.cpp constructs the request using the peer UID and PID obtained by the daemon. get_su_info() considers global root-access settings and a multi-user mode. check_db() looks up a UID policy whose until is zero or later than the current time (WHERE uid=%d AND (until=0 OR until>…)); the stored row also includes logging/notification fields (SELECT policy, logging, notification). If policy remains QUERY, su_daemon_handler() calls app_request(ctx). In this pinned function, a negative result from app_request becomes DENY; a nonnegative reply is read and converted to a policy result. A denied request is rejected before the root child is forked.
The policy key is an Android UID, not an arbitrary package name supplied by the requesting process. Android app UIDs include user/profile identity, so the selected multi-user mode matters. Source also contains prune_su_access(), which removes policy rows for app UIDs no longer installed when that cleanup is invoked (the daemon calls it on the zygote-restart request path, daemon.cpp). This is a policy lifecycle mechanism, not proof that every UID reuse race is impossible or that cleanup occurs synchronously at uninstall.
Child process: identity is more than one number
The unprivileged app does not become root merely because its request was approved. There are separate processes: the client submits the request, the privileged daemon checks it, and a daemon-created child runs the command. set_identity() works from the child's existing retained authority; its attempt to restore effective UID 0 is not an operation an ordinary app can perform successfully. The credential-transition chapter explains that distinction with before/after identities and the actual adbd decision function.
After authorization, the daemon forks; the parent waits for the child and reports a return code. The child sets up streams/session, chooses a mount namespace mode, prepares environment, optionally requests a SELinux exec context, calls set_identity(), and executes the requested shell/command (pinned su_daemon.cpp: xunshare(CLONE_NEWNS), /proc/self/attr/exec, set_identity()). A mount namespace is a view of mounted filesystems; it is not a UID or SELinux label. The SELinux context written to /proc/self/attr/exec requests a context transition, which remains subject to active policy.
Process authority also depends on real/effective/saved UID/GID, supplementary groups, capability sets, SELinux context, open file descriptors, namespace and kernel policy. The fact that the resulting process has UID 0 does not prove every operation is allowed. Conversely, it is not correct to claim that stock SELinux always constrains every rooted process: a root implementation can modify policy or establish a different context. If it does, the effective TCB/policy has changed and must be inspected rather than inferred from stock rules.
Authorization lifecycle: evidence and unknowns
The following table is deliberately scoped to the v28.1 source paths above. “Unknown here” means this walkthrough does not establish the behavior; it is not a claim that the project has no behavior.
| Event/question | What the pinned source establishes | What must not be asserted from this path alone |
|---|---|---|
| Request identity | Daemon passes peer UID/PID; policy lookup is UID-oriented and multi-user settings can remap/evaluate identities. | That package display name is the security identity, or that UID uniquely identifies an app forever. |
| Manager absent/unavailable | get_su_info() refuses a pending query if no manager UID is found; app_request() failure (fd < 0) is converted to deny. | Exact user-visible message, timeout duration, retry policy, or behavior under every manager/IPC failure. The broader app_request implementation and runtime path must be inspected for those. |
| Expiring grant | DB query accepts until=0 or until > time(nullptr). | Exact UI semantics, clock-change handling, or whether a particular user sees a prompt at expiration without tracing policy/cache paths. |
| Explicit revocation after child starts | Authorization is checked before fork; child then executes. | That revocation kills an already-running command. This trace does not show such termination; do not promise immediate revocation of an existing process. |
| App removal/reinstall and UID reuse | prune_su_access() can delete rows for app UIDs no longer installed; daemon calls it on the observed zygote-restart request path. | Immediate uninstall cleanup, all reinstall/UID-reuse ordering, or behavior if cleanup does not run. Verify the exact lifecycle source or test it on the exact release. |
| User/profile | Source has owner-only, owner-managed, and per-user modes and uses Android UID/user helpers. | One universal policy mapping across all configuration settings or Android versions. |
| Manager compromised | Manager participates in query/presentation and policy management; daemon remains an enforcement component. | That a compromised manager is harmless, or that daemon/source checks prevent all malicious policy changes. Threat-model manager and daemon as TCB components. |
| Reboot/update | This source demonstrates boot initialization and persistent policy database use. | Grant migration, database compatibility, restoration, or update survival for other versions without tracing update code and testing. |
For the earlier image transformation, follow the unpack → ramdisk-state check → payload/configuration insertion → conditional patching → repack stages in v28.1 scripts/boot_patch.sh. This changes boot artifacts before the daemon mediates any request. The fuller mounts and systemless chapter explains that pipeline, bind mounts, propagation, persistent payloads, and why different processes can see different files without changing the base /system bytes.
For an exact lifecycle answer, trace the relevant pinned implementation and write down source evidence; if source does not show a timeout/revocation guarantee, label it unknown. Inside kernel root then contrasts this broker-created child with a historical kernel-side credential transition.
Practice#
Source-only, safe exercise: select the exact v28.1 commit; follow daemon.cpp request dispatch → su_daemon_handler() → get_su_info()/check_db() → optional app_request() → denial/fork → namespace/context/identity setup. Complete the lifecycle table for one question using the corresponding source function. If using an owned disposable lab, test only ordinary allow/deny with a harmless command and record version, build, user/profile and result. Do not test on a daily-use device; do not infer runtime behavior from the source-only exercise.
Knowledge check#
- What identity does the pinned daemon use for the initial policy decision?
- What does the v28.1 path do if no manager is found for a still-pending query?
- Does UID 0 imply a particular SELinux domain or mount namespace?
- Does the cited child path prove revoking a grant kills a running child?
- What does
prune_su_access()establish, and what does it not establish?
Answers#
- The kernel-reported peer UID (with PID also used in request context); package labels are not a substitute for that credential.
- It changes the unresolved query to no access; a failed manager request is also converted to denial in the cited function.
- No. Those are separate state dimensions, and a requested SELinux transition is policy-dependent.
- No. It shows authorization before fork and then child execution, not a kill-on-revoke mechanism.
- It removes rows for app UIDs it finds no longer installed when called. It does not prove immediate cleanup, no UID reuse edge case, or universal behavior across releases.
Checkpoint#
Submit the completed lifecycle evidence/unknown table and a process-state diagram. Ready if every claim is pinned to v28.1 and unknowns stay unknown; revise if a manager dialog is treated as the whole enforcement boundary or revocation is assumed to kill existing children; not yet evidenced if conclusions rely on another release’s behavior. Remediation: trace the function named in the source and, where needed, design a harmless test on an exact disposable build.
Sources#
- Magisk v28.1, tag commit
218327f92b572c183718e0fe722c2babf293535f:init.cpp,daemon.cpp,su_daemon.cpp. Implementation claims are pinned to this revision; no runtime lifecycle tests were run. - AOSP, SELinux concepts and Verified Boot, current docs, accessed 2026-10-26.
- Magisk official repository and documentation, third-party, moving project documentation; not used to generalize pinned implementation behavior.
Next: Inside kernel root, followed by mounts and the systemless file view.