init and init.rc
One small C++ program is PID 1, mounts the world, loads the security policy, starts every daemon and restarts them when they die. Its configuration language is the closest thing Android has to an init system.
Every Android device boots the same binary first. It is called init, it lives at /system/bin/init, it runs as PID 1, and it is responsible for everything between "the kernel is running" and "the framework exists". If it fails, the device does not boot; if it works, you never think about it.
init is also the most underrated program in Android. It is a service manager, a property server, a mount orchestrator, a SELinux policy loader, a reaper of orphaned processes and a small declarative language runtime. Learning to read init.rc gives you a direct view of how the system is assembled — which services exist, what they depend on, and what order the platform actually needs.
TL;DR#
- init runs in three phases: first stage (mount the basics), SELinux setup (load the policy), second stage (parse the rc files and start services).
- is written in the Android Init Language, with five statement types: actions, commands, services, options, imports.
- Ordering is declarative. Services belong to classes; actions fire on triggers; init works out when to run them.
- The boot triggers form a spine:
early-init,init,late-init, thenearly-fs,fs,post-fs,late-fs,post-fs-data,zygote-start,early-boot,boot. - Properties are init's other job. It owns the property service that every process reads through
getprop, andproperty:name=valuetriggers let rc files react to state changes.
Mental model#
Think of init as the stage crew of a theatre that arrives before the building opens.
The crew does not act. It unlocks doors (mounts filesystems), sets the safety rules (loads SELinux policy), and then follows a call sheet that says which scene needs which set piece. Some set pieces are needed for the first scene; some wait for a cue. If a piece falls over, the crew puts it back — unless the piece was marked as one-shot.
The call sheet is init.rc. Each line is either "here is a piece of equipment and how to start it" (a service) or "when this cue happens, do these things" (an action).
The cue can be an event — "the filesystem phase has started" — or a condition — "when property X equals Y". That second kind is why init can react to the world changing long after boot.
Where the analogy breaks#
Stage crew work for a fixed show. init runs an unbounded set: vendors add services, APEXes add services, the framework starts and stops things through properties. The call sheet is assembled at boot from a dozen directories, and the same device can run different sets depending on which APEXes are active.
The other break is that init is not just coordinating. It is the property server. Every getprop in the system is a call into init's property service, which means a stalled init stalls the entire system, not just the boot.
The big picture#
Text description of this diagram
The boot pipeline, with init occupying two of the eight stages. First-stage init mounts /dev, /proc and the partitions holding system code, then re-executes itself to load the SELinux policy. Second-stage init parses init.rc and every imported rc file, then starts services by class and by trigger. The pipeline continues through zygote, system_server, and the launcher. Reading the diagram from the init stage onward shows what init is responsible for starting: it starts zygote on the zygote-start trigger, and zygote in turn starts system_server.
The two init stages in that diagram are the same binary executed three times: once as /init from the ramdisk, once with the argument selinux_setup, and once with the argument second_stage. Each execution has a different job and a different SELinux context, which is why it re-executes rather than trying to switch modes in place.
Deep dive#
Phase 1: first stage#
First-stage init runs from the ramdisk, before /system is mounted. It is deliberately minimal:
Mount /dev, /proc, /sys
Early-mount the partitions that hold system code (/system, /vendor, ...)
Switch root if the device uses a system-as-root layout
Re-exec /system/bin/init selinux_setupOn a device with dynamic partitions, "early-mount /system" is not a simple mount. The super partition holds metadata describing where each logical partition lives, and first-stage init must read that metadata and create device-mapper devices for system, vendor, product and the rest before any of them can be mounted. The kernel has no idea what super is; user space does the partitioning.1
Phase 2: SELinux setup#
The second execution of init exists to load the policy. It compiles (optionally) and loads the binary SELinux policy, then re-executes as second_stage. From this point on every process init starts inherits a domain, and every access is checked against the policy.3
The ordering matters. If the policy were loaded later, services started before it would run unconfined, which would defeat the purpose. Loading it in a separate process image means there is no window in which a half-initialised init is running with the wrong context.
Phase 3: second stage#
Second-stage init assembles the configuration. It loads /system/etc/init/hw/init.rc as the primary script, then imports:
/system/etc/init//system_ext/etc/init//vendor/etc/init//odm/etc/init//product/etc/init//apex/*/etc/*rcfor every active APEX
That last import is why APEXes can ship their own services. A Mainline module that needs a daemon adds an rc file to its APEX, and init picks it up without the system image knowing anything about it.4
Directory imports are not recursive. If you add a service definition in a subdirectory of /vendor/etc/init, it will silently not be loaded. That is one of the most common mistakes when adding a vendor service.
The language#
Five statement types, and that is genuinely all of it.5
Actions group commands under a trigger:
on <trigger> [&& <trigger>]*
<command>An action can have multiple property triggers, but at most one event trigger.
Services define a program init can launch:
service <name> <pathname> [ <argument> ]*
<option>Names are unique; a duplicate definition is ignored, and the first one wins.
Options modify a service: class, user, group, socket, disabled, oneshot, seclabel, capabilities, priority, onrestart, task_profiles.
Commands are built-ins used inside actions: start, stop, class_start, setprop, exec, exec_start, mount_all, trigger, wait, wait_for_prop, write, chown, mkdir, symlink, restorecon.
Imports pull in another file.
A realistic service definition looks like this — the structure is from the real init.zygote64.rc, with the argument list abbreviated:
service zygote /system/bin/app_process64 -Xzygote /system/bin --zygote --start-system-server
class main
priority -20
user root
group root readproc reserved_disk
socket zygote stream 660 root system
onrestart write /sys/power/state on
onrestart write /sys/power/wake_lock zygote_kwl
onrestart restart audioserver
onrestart restart netd
task_profiles ProcessCapacityHigh MaxPerformance
critical window=${zygote.critical_window.minute:-off} target=zygote-fatalRead it as a set of guarantees. class main means it starts with the main class. socket means init creates the listening socket before the process starts and passes it in, so there is no race where a client connects before the server is listening. onrestart means that if zygote dies, init restarts audioserver and netd too, because they depend on state zygote held. critical means repeated crashes cause a reboot rather than an infinite restart loop.
That last option is worth dwelling on. Without critical, a service that crashes on startup would be restarted forever, burning battery and filling the log. With it, init gives up and reboots the device, which is the correct behaviour for something as load-bearing as the zygote.
Triggers: the boot spine#
The built-in event triggers, in the order late-init fires them:
| Trigger | What is true at this point |
|---|---|
early-init | Nothing is mounted; init sets permissions and kernel tunables |
init | /system is available; early services can start |
early-fs | Filesystem mounts can begin |
fs | /system and /vendor are mounted |
post-fs | Properties from /system and /factory are loaded |
late-fs | Late mounts; early HALs start |
post-fs-data | /data is mounted and decrypted |
load-bpf-programs | eBPF programs can be loaded |
bpf-progs-loaded | eBPF programs are available |
zygote-start | The framework can start |
early-boot | Most services are up |
boot | Boot is complete |
late-init is the dispatcher that fires this chain in order. You can see it in the real file:
on late-init
trigger early-fs
trigger fs
trigger post-fs
trigger late-fs
trigger post-fs-data
trigger load-bpf-programs
trigger bpf-progs-loaded
trigger zygote-start
trigger firmware_mounts_complete
trigger early-boot
trigger bootEach trigger fires the actions attached to that event. The ordering is explicit here, but the contents of each stage are open: any rc file on the device can add an action to any of these triggers, which is how vendors and APEXes hook into the boot without editing the primary script.6
One service in that chain is worth calling out because it connects boot to updates:
on zygote-start
wait_for_prop odsign.verification.done 1
# A/B update verifier that marks a successful boot.
exec_start update_verifier
start statsd
start zygotewait_for_prop blocks until the APEX signature check has finished, and exec_start update_verifier runs the program that marks a newly updated slot as successful. Boot ordering is not only about dependencies; it is also about when it is safe to trust the code that is about to run.
Property triggers#
The other kind of trigger is a condition on a system property:
on property:sys.boot_completed=1
bootchart stopProperty triggers behave differently from event triggers. They are checked once at the end of the boot event, then again whenever the named property changes, and again when another trigger in the same action is evaluated. The value can be * to match anything.
This is how the framework communicates with init. When system_server finishes starting, it sets sys.boot_completed; init's action fires; bootchart stops. When a property like persist.sys.usb.config changes, init reconfigures USB. The property namespace is the shared bus between init and everything else.
Services, classes and failure#
A service's class determines when it starts. The standard classes are:
core— logd, servicemanager, vold and other essentialsmain— zygote and the framework-adjacent serviceslate_start— services that need/datahal— binderized HAL daemonsearly_hal— HALs needed before/datacan be decryptedcharger— the charging UI
class_start <class> starts everything in a class; class_start on core and hal happens during the boot trigger, and main/late_start on the nonencrypted trigger.
If a service dies, init restarts it unless it is marked oneshot. If it dies repeatedly, init's restart backoff grows; if it is marked critical, init reboots instead.
The property service#
init owns the property area, and getprop/setprop are thin clients over a Binder call to init. Properties have types (string, int, bool, and others), prefixes that determine who may set them (ro. is read-only after set, persist. survives reboot, ctl. controls services), and SELinux labels that determine who may read or write them.
That last point is the one people miss. Property access is policed: an app cannot read ro.boot.verifiedbootstate unless the policy allows it, and it certainly cannot write persist.sys.*. The property store is part of the security surface, not just a convenience.
How it evolved#
init has grown rather than changed. The language has been stable for years; what has changed is where the configuration comes from and what init can mount.
| API | Version | What changed |
|---|---|---|
| 26 | 8.0 Oreo |
|
| 28 | 9 Pie |
|
| 29 | 10 |
|
| 30 | 11 |
|
The three structural additions are worth naming:
- APEX imports (Android 10 onward). Modules can ship their own services, which is why
/apex/*/etc/*rcis in the import list. - Dynamic partitions (Android 10). First-stage init now creates logical partitions from
supermetadata before mounting anything. - First-stage mount and
init_boot(Android 13). The ramdisk moved to its own partition, and the mount logic moved with it.
The language itself has stayed remarkably small, which is a feature: a configuration language that only grows when it must is one that stays readable.
Under the hood#
- README.mdthe complete language reference and the three phases
- init.rcthe primary script; read late-init first, then zygote-start
- init.zygote64.rcthe zygote service in full
- service.cppwhat happens when a service starts, fails or restarts
- property_service.cppthe property store and its permission checks
- first_stage_init.cppmounting before /system exists
Curiosity corner#
- init creates the socket before the process exists. That is why a client can connect to
servicemanagerthe instant it starts — the socket was already listening. - A service name is a namespace.
ctl.startandctl.stopproperties let privileged processes start and stop services by name, which is how the framework restartsnetdor toggles a daemon without a reboot. onrestart restart netdin the zygote definition is a dependency declaration. If zygote dies, the network daemon is restarted too, because the framework's state it depended on is gone.- The rc import list is a map of the platform. System, system_ext, vendor, odm, product and APEXes each contribute; anything not in those directories is invisible to init.
Try it#
adb shell ps -A | head -20
adb shell getprop | grep "^[init.svc"
adb shell getprop | grep init.svc_debug_pid
adb shell dmesg | grep -i "init:"
Example output
init
ueventd
logd
servicemanager
vold
zygote
...
[init.svc.zygote]: [running]
[init.svc.surfaceflinger]: [running]
[init.svc.bootanim]: [stopped]
[init.svc_debug_pid.zygote]: [612]
[ro.boottime.zygote]: [4821902113]init.svc.<name> is init's own record of each service's state. running, stopped, restarting and crashed are the values you will see. If a service you expected is missing entirely, its rc file was not imported — check the directory.
Build it#
The N03 lab writes an init service from scratch, including the rc file, the SELinux policy and the class assignment. It is the fastest way to learn why an rc file that looks correct can still be silently ignored.
Check yourself#
Check yourself
Which stage runs first after the power button is pressed?
Sources#
Sources
Every version number, API level, class name and date on this page is traced to one of these. Pages are re-verified against them; last_verified above says when.
- docsDynamic partitionssource.android.comaccessed 2026-10-27↩
- docsFile-Based Encryption (FBE)source.android.comaccessed 2026-10-27↩
- docsSELinuxsource.android.comaccessed 2026-10-27↩
- docsAPEX file formatsource.android.comaccessed 2026-10-27↩
- sourcesystem/core/init/README.mdandroid.googlesource.comaccessed 2026-10-27↩
- sourcesystem/core/rootdir/init.rcandroid.googlesource.comaccessed 2026-10-27↩