How a process becomes root

Follow actual credential authority, adbd restarts, brokered children, and setuid constraints.

9 min readChapter 38 of 50Advanced
In this chapter

“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:

FieldQuestion it helps answer
Real UIDWhich user identity is associated with this process?
Effective UIDWhich UID participates in many access checks?
Saved set-user-IDWhich effective identity can some credential operations restore?
Filesystem UIDWhich UID participates in filesystem access checks on Linux?
GIDs and supplementary groupsWhich group-based access applies?
Capability setsWhich particular privileged operations are available?
LSM security stateWhich 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#

Authority before the UID changes
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.
Conceptual model. Authority origins, not installation instructions or equivalent safety claims. Root identity, SELinux state, capabilities, and boot acceptance remain separate.

Every root story needs an authority origin:

  1. Retain authority: a privileged parent starts a daemon; a supported debug configuration allows it not to drop privilege.
  2. Delegate authority: an already-privileged broker authenticates an unprivileged client and starts a separate privileged command.
  3. Change the kernel's rules: owner-installed kernel-root code mediates a selected credential transition inside the kernel.
  4. 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:

  1. If adbd already has UID 0, it reports that fact.
  2. If __android_log_is_debuggable() is false, it reports that adbd cannot run as root in production builds and returns.
  3. Otherwise, it requests service.adb.root=1 and 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.secure determines the default drop decision.
  • A debuggable build plus service.adb.root=1 changes that decision to retain privilege.
  • An explicit service.adb.root=0 forces 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.securedebuggableroot requestComputed drop decision
1nounsetdrop
1no1drop; production restart request is also refused earlier
1yesunsetdrop
1yes1retain
0yes0drop, 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.

python
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.

text
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#

  1. Why does an app's setuid(0) normally fail?
  2. Which process changes in adb root: an arbitrary app or a restarted daemon?
  3. Does Magisk authorization normally rewrite the requesting app's UID?
  4. Why can a setuid-marked executable still run without elevated identity?

Answers#

  1. It lacks the required existing identity/capability authority for that transition.
  2. A newly started adbd follows the supported retain/drop path; subsequent shells reflect that daemon configuration.
  3. No. An authorized command runs in a separate daemon-created child.
  4. 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#

A little further beneath the surface.

Check your understanding with the checkpoint. Marking a chapter as read saves a bookmark, not a proficiency assessment.