“The tool gives root” skips the most important question: which already-authorized component changes which process's credentials? An ordinary app cannot promote itself by choosing the number zero. This chapter follows the actual transition, first through AOSP's debug daemon and then through a root broker.
Before you start#
Read Rooting methods, Android's security model, Linux processes, and Concurrency and lifetime. The source baseline is AOSP android-14.0.0_r1. Linux credential rules are a model for interpreting that source, not permission to experiment on a production phone. The required exercises run entirely on paper or an unprivileged host.
Learning goals#
Explain real, effective, saved, and filesystem identities; locate the authority behind a root transition; trace adb root through a restart rather than an imaginary self-promotion; and distinguish a privileged child from the unprivileged app that requested it. Predict why setuid(0), a copied su binary, or an installed manager can fail.
Credentials are state, not a username string#
Linux stores credentials associated with tasks. The important fields include:
| Field | Question it helps answer |
|---|---|
| Real UID | Which user identity is associated with this process? |
| Effective UID | Which UID participates in many access checks? |
| Saved set-user-ID | Which effective identity can some credential operations restore? |
| Filesystem UID | Which UID participates in filesystem access checks on Linux? |
| GIDs and supplementary groups | Which group-based access applies? |
| Capability sets | Which particular privileged operations are available? |
| LSM security state | Which SELinux domain and mandatory policy apply? |
The Linux credentials documentation describes credential objects and their lifecycle. POSIX threads normally present process-wide credentials through libc coordination, while the kernel represents task credentials; do not casually replace credentials underneath one thread and assume the entire process remains coherent.
Real, effective, saved, and filesystem UIDs often match in Android app processes. They are separate because a privileged program may need to distinguish original identity, current authority, and temporarily retained authority. The special rules are specified by setuid(2) and setresuid(2), not by the executable's name.
For an ordinary app whose real/effective/saved UIDs are all 10123 and which lacks the necessary capability, asking setuid(0) fails with EPERM. The requested number is not a credential. With appropriate CAP_SETUID authority in the governing user namespace, a process can change UIDs according to the syscall rules. Namespace-relative root is not automatically root over the host; Android's normal app sandbox should not be pictured as one independent root user namespace per app.
Changing UID also interacts with capabilities. Effective, permitted, inheritable, bounding, and ambient capability sets have distinct meanings; exec and UID transitions can recalculate or clear them. CAP_SETUID permits particular identity operations, not every file operation. CAP_DAC_OVERRIDE is not an SELinux override. Consult capabilities(7) before reasoning from a single UID or capability bit.
The four authority paths#
- Retain authority
- A supported debug daemon restart does not drop its privileged starting identity.
- Delegate authority
- An existing privileged broker authorizes and creates a command child; the app stays unprivileged.
- Change kernel rules
- Owner-installed compatible kernel code mediates selected task credential changes.
- Exploit a defect
- A vulnerability violates an intended boundary; no exploit chain is implemented here.
Every root story needs an authority origin:
- Retain authority: a privileged parent starts a daemon; a supported debug configuration allows it not to drop privilege.
- Delegate authority: an already-privileged broker authenticates an unprivileged client and starts a separate privileged command.
- Change the kernel's rules: owner-installed kernel-root code mediates a selected credential transition inside the kernel.
- Exploit a defect: a vulnerability crosses a boundary that was supposed to prohibit the transition. This is neither required nor implemented by the exercises here.
The first three require the booted system to contain and accept the corresponding privileged component. Installing a UI application supplies none of those origins by itself. Unlocking a bootloader may enable an owner to install a different system; it does not directly edit a running app's credentials.
Worked source trace: AOSP adbd retains root#
Open the Android 14 restart_service.cpp, function restart_root_service:
- If adbd already has UID 0, it reports that fact.
- If
__android_log_is_debuggable()is false, it reports that adbd cannot run as root in production builds and returns. - Otherwise, it requests
service.adb.root=1and reports that adbd is restarting.
The property is a request to the existing control plane, not a capability an app can mint. In pinned sockets.cpp, eligible root/unroot service sockets are marked exit_on_close; local_socket_destroy() exits the daemon after that socket closes. Init supervises the daemon according to its service configuration. The old daemon does not recover irreversibly dropped privileges by an ordinary shell executing setuid(0); a newly started daemon evaluates its starting privilege configuration again. Existing ADB connections can disconnect during this lifecycle.
Now inspect daemon/main.cpp, should_drop_privileges() and drop_privileges():
ro.securedetermines the default drop decision.- A debuggable build plus
service.adb.root=1changes that decision to retain privilege. - An explicit
service.adb.root=0forces dropping. - The drop path configures minijail with
AID_SHELL, groups, and capability handling. - The retain path can establish a configured root SELinux label; failure is not silently equivalent to a successful transition.
For the usual debug-build combination, the decision can be worked out without a device:
ro.secure | debuggable | root request | Computed drop decision |
|---|---|---|---|
| 1 | no | unset | drop |
| 1 | no | 1 | drop; production restart request is also refused earlier |
| 1 | yes | unset | drop |
| 1 | yes | 1 | retain |
| 0 | yes | 0 | drop, despite the default |
These rows describe the pinned decision function, not all build-time or vendor constraints. Editing a reported ro.debuggable string is not a universal way to obtain a differently built daemon, change SELinux policy, or establish an authorized boot chain.
A runnable decision model
HOST, Python 3; no ADB, credential changes, or files required. Save this complete original teaching example in your own practice directory and run it with python3 model.py. It models only the decision above, not init, minijail, property authorization, or real privilege.
def should_drop(secure, debuggable, request):
drop = secure
if debuggable and request == "1":
drop = False
if request == "0":
drop = True
return drop
assert should_drop(True, False, "1") is True
assert should_drop(True, True, "") is True
assert should_drop(True, True, "1") is False
assert should_drop(False, True, "0") is True
print("4 credential-decision cases passed; no privileges changed")
The negative production case is essential: a model that says any root request grants root has missed the authority check. Successful assertions are host-model results, not Android results.
Worked process tree: a broker launches a root child#
Magisk uses a different route. Its already-privileged daemon receives the request, determines the socket peer's identity, checks policy, and forks an authorized child. In pinned v28.1 su_daemon.cpp, the denial branch precedes xfork(). In the child, set_identity() invokes identity-setting operations before the requested shell is executed. The helper first attempts to restore effective UID 0 in a process that already has the daemon's retained authority; it is not a demonstration that arbitrary apps can call seteuid(0) successfully.
app, UID 10123 privileged daemon
su client ── request ──────→ authenticate peer + authorize
waits for result └─ fork child → prepare identity → exec command
receives streams/status ←───────────────────────────────────────┘
app remains UID 10123
Why a separate child? The broker can retain its own service identity while a command has its own process lifetime, environment, streams, and exit status. The manager provides consent/policy interaction; the daemon enforces it. A granted request is powerful precisely because the child can act outside the original app sandbox. It does not make every thread of the requesting app root. See the Magisk walkthrough for policy and failure paths.
Why a copied su binary is not enough#
A traditional Unix set-user-ID executable can acquire an effective UID from trusted file metadata during execve. That requires privileged installation/ownership, an applicable executable format, mount and security policy that permit the transition, and absence of suppressing conditions. The kernel does not trust the text “su” in a filename. nosuid, no_new_privs, tracing restrictions, SELinux, and executable handling affect the result; Linux ignores set-user-ID bits on scripts. See execve(2).
Android root frameworks therefore cannot be explained as “download su and chmod it.” Creating a root-owned setuid binary is itself privileged; a production image can prevent its installation and transition. A brokered su client may be an ordinary unprivileged executable communicating with a daemon. A kernel implementation may intercept an execution path under an allowlist instead. Same command name, different authority origin.
Practice#
Produce two before/after process tables: AOSP debug restart and Magisk request. For each process include creator, UID, capability/domain evidence available, and lifetime. Run the decision model, then explain why its fourth case drops despite an insecure default.
Optional live evidence belongs in the disposable root lab, not on a retail phone. Record id, the Uid:/Gid: and capability fields from your own process's /proc/self/status, and the SELinux context where permitted. Four Uid: values mean real/effective/saved/filesystem, not four users logged in. A capability hex mask requires decoding against the matching kernel definitions; a large number is not a security score.
Knowledge check#
- Why does an app's
setuid(0)normally fail? - Which process changes in
adb root: an arbitrary app or a restarted daemon? - Does Magisk authorization normally rewrite the requesting app's UID?
- Why can a setuid-marked executable still run without elevated identity?
Answers#
- It lacks the required existing identity/capability authority for that transition.
- A newly started adbd follows the supported retain/drop path; subsequent shells reflect that daemon configuration.
- No. An authorized command runs in a separate daemon-created child.
- Mount flags,
no_new_privs, tracing/executable rules, or security policy can suppress or deny the transition; the file may also lack valid privileged ownership.
Checkpoint#
Ready when both process tables identify the authority origin, original client identity, enforcement point, and resulting child/daemon identity, and the model's four cases pass. Revise if a property, APK, filename, or syscall argument is treated as authority. Return to the pinned should_drop_privileges and denial-before-fork branches, then redraw the transition without using the phrase “the tool gives root.” Runtime root remains not yet evidenced unless observed on an identified target.
Sources#
- AOSP Android 14
daemon/main.cpp,restart_service.cpp, andsockets.cpp— drop/retain decision, root request, and exit-on-close lifecycle. - Linux 6.1 credentials — credential objects and lifecycle; not an endorsement of third-party kernel changes.
- Linux man-pages
setuid,setresuid,capabilities, andexecve— syscall and execution rules, current documentation. - Magisk v28.1
su_daemon.cpp— peer-policy, fork, andset_identityimplementation; historical pin, not a current compatibility claim.