Zygote
Builder tracknerd

N03 — Write an init service

Add a native daemon to the boot sequence: an init.rc service block, a tiny binary, an SELinux domain and the property that proves it started. The most self-contained platform change you can make.

6 minAPI 1–37 · 17verified 2026-10-27

A daemon started by init is the smallest honest platform change. You do not touch the framework, you do not touch Binder, and the failure modes are visible in logcat and the kernel log. It still needs a full platform build and a boot, so it is a nerd project.

TL;DR#

  • init reads .rc files at boot and starts the services they declare.
  • service <name> <path> [args] declares a service; the options are class, user, group, oneshot, seclabel and socket.
  • on <trigger> declares an action; start <service> launches the service from it.
  • class_start <class> starts a whole class at once, which is how hal and core come up.
  • A new service needs SELinux policy, or the exec is denied.

What you are building#

A small native binary that writes a property once at boot and then exits, started by init from an .rc file you add. It is deliberately trivial: the value is not the program, it is the chain —

binary → Android.bp module → .rc service block → SELinux domain → property visible over adb.

The rc language in one table#

Every option below comes from the init README.1

SyntaxMeaning
service <name> <path> [ <argument> ]*Declares a service. Names must be unique; a duplicate is ignored and logged.
class <name> [ <name>* ]Which class the service belongs to. Without it, the service is in default.
user <username>Drops to this UID before exec. Defaults to root.
group <groupname> [ <groupname>* ]Primary group plus supplemental groups. Defaults to root.
oneshotDo not restart the service when it exits.
seclabel <context>The SELinux context to exec under.
socket <name> <type> <perm> [...]Creates /dev/socket/<name> and passes the fd to the service.
on <trigger> [&& <trigger>]*Declares an action, run when the trigger fires.
start <service>Starts a service if it is not already running. Asynchronous.
class_start <class>Starts every service in the class.

early-init, init, late-init, early-boot and boot are the built-in boot triggers, in that order. boot runs after the early-boot actions complete. The platform init.rc is where you see them used together: on early-init, on boot, and the class_start lines that bring up hal and core.2

Steps#

  1. Write the daemon

    A minimal C program that calls the property setter and returns. It does not need to be a long-lived process; a oneshot service that does one thing at boot is enough to prove the wiring. Keep it somewhere under system/ or device/ in the tree, in a directory with its own Android.bp.

    You should seeA binary that runs, sets a property and exits cleanly.
  2. Add the Android.bp module

    Declare a cc_binary with the source file, and make sure the module is installed into the image (the system partition for a platform daemon). If the binary is not installed, init will try to exec a path that does not exist and log the failure.

    You should seeThe module builds and its output is in the system image.
  3. Write the service block

    Add an .rc file, either appended to an existing device .rc or as a new file that is copied into the image. The following is illustrative — the names are invented for the guide:

    # Illustrative example, not a file from AOSP.
    service toy-daemon /system/bin/toy_daemon
        class late_start
        user system
        group system
        oneshot
        seclabel u:r:toy_daemon:s0
     
    on property:sys.boot_completed=1
        start toy-daemon

    Read the block as a sentence: "there is a service called toy-daemon, it is the binary at /system/bin/toy_daemon, it runs as system, it does not restart, and it runs under this SELinux label." The action below it says "when the boot-completed property is set, start it".

    1
    You should seeinit parses the block without a syntax error.
  4. Hook it into a boot trigger

    A service block alone does not start anything. Either put it in a class that is started by an existing class_start line, or add an on action that calls start. init.rc in system/core shows the real pattern: on early-init, on boot, and class_start hal / class_start core.

    2

    Ordering is by trigger, so choose the trigger that matches your dependencies. A daemon that needs the data partition must not run at early-init.

    You should seeThe service actually starts.
  5. Declare the SELinux domain

    Create a .te file in the device policy directory and declare the domain. The customize documentation shows the shape: type dhcp, domain; gives the new type the domain attribute. For a daemon spawned by init, the init_daemon_domain macro encodes the transition and the basic communication with init.3

    The domain name must match the seclabel in the service block. A mismatch shows up as avc: denied on the exec, and the service fails to start even though the binary exists.

    You should seeNo `avc: denied` for the exec.
  6. Point the build at your policy

    Add your policy directory to the device's BOARD_SEPOLICY_DIRS in BoardConfig.mk. The SELinux implement page gives the pattern:

    BOARD_SEPOLICY_DIRS += \
            <root>/device/manufacturer/device-name/sepolicy

    Policy from that directory is compiled into the final kernel policy.4

    You should seeThe policy is compiled into the image.
  7. Build and boot

    Build the image and boot your target as in N01. The exact target string comes from the AOSP build page for your branch.5

    You should seeThe device boots and the property appears.
  8. Check the result
    adb shell getprop | grep toy
    adb logcat -b all -s init
    adb shell dmesg | grep avc

    The property proves the binary ran. The init log shows the service being started and its exit. The avc grep is how you find a missing policy rule — an empty result is what you want. The init README is the grammar of everything you wrote above, and the place to look when a block is rejected.

    1
    You should seeA property, a log line and a process that ran.

Build and check#

Build, boot, and read the traces
source build/envsetup.sh
lunch <your target from N01>
m
adb shell getprop | grep toy
adb logcat -b all -s init
adb shell dmesg | grep avc
Example output
Illustrative — no build was executed in this run.

<your target from N01> is a placeholder deliberately. The exact lunch target should be copied from the AOSP build page for your branch.5

The files you touched#

Policy is a whole subject of its own; the SELinux overview is the entry point, and the implement page is the procedure you followed above.34

  • README.mdthe authoritative description of the service and action syntax
  • init.rcthe platform init.rc, with the early-init and boot actions
  • the base policy; read it, do not edit it
  • where your device-specific .te file and BOARD_SEPOLICY_DIRS point

Verification#

A service that started and exited cleanly leaves three traces: the property, the init log line, and no denial in dmesg. A service that failed to start leaves a log line naming the reason — missing binary, denied exec, or a policy transition with no matching domain. Read the init log first; it usually names the problem.

What can go wrong#

  • The binary is not in the image. The module is not installed, or it installed to a partition the service path does not point at.
  • The service block never runs. A service block does not start anything by itself; you still need an on action with start, or a class_start for its class.
  • avc: denied on exec. The seclabel has no domain, or the domain is not allowed to execute the binary's label.
  • The service restarts forever. You did not mark it oneshot, and it exits immediately, so init restarts it. The log fills with start/exit pairs.
  • It runs too early. The filesystem or the property service it depends on does not exist at that trigger. Move it later in the sequence.
  • You edited system/sepolicy directly. The documentation says not to; add device policy under your BOARD_SEPOLICY_DIRS instead.4

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.

  1. sourcesystem/core/init/README.mdandroid.googlesource.comaccessed 2026-10-27↩
  2. sourcesystem/core/rootdir/init.rcandroid.googlesource.comaccessed 2026-10-27↩
  3. docsSELinuxsource.android.comaccessed 2026-10-27↩
  4. docsImplement SELinuxsource.android.comaccessed 2026-10-27↩
  5. docsBuild Androidsource.android.comaccessed 2026-10-27↩
  6. sourceAndroid Code Search (cs.android.com)cs.android.comaccessed 2026-10-27↩