---
url: https://kernelsu.org/guide/module.md
description: >-
  Guide to developing KernelSU modules, including module layout, module.prop,
  the customize.sh installer, boot scripts, and late-load mode.
---

# Module guide

KernelSU provides a module mechanism that achieves the effect of modifying the system directory while maintaining the integrity of the system partition. This mechanism is commonly known as "systemless".

The module mechanism of KernelSU is almost the same as that of Magisk. If you're familiar with Magisk module development, developing KernelSU modules is very similar. You can skip the introduction of modules below and just read [Difference with Magisk](difference-with-magisk.md).

::: warning METAMODULE ONLY NEEDED FOR SYSTEM FILE MODIFICATION
KernelSU uses a [metamodule](metamodule.md) architecture for mounting the `system` directory. **Only if your module needs to modify `/system` files** (via the `system` directory) do you need to install a metamodule (such as [meta-overlayfs](https://github.com/tiann/KernelSU/releases)). Other module features like scripts, sepolicy rules, and system.prop work without a metamodule.
:::

## WebUI

KernelSU's modules support displaying interfaces and interacting with users. For more details, refer to the [WebUI documentation](module-webui.md).

## Module Configuration

KernelSU provides a built-in configuration system that allows modules to store persistent or temporary key-value settings. For more details, refer to the [Module Configuration documentation](module-config.md).

## One-shot Scripts

A module ZIP can also be a one-shot script that the KernelSU manager runs once instead of installing. For more details, refer to the [One-shot Scripts documentation](module-script.md).

## BusyBox

KernelSU ships with a feature-complete BusyBox binary (including full SELinux support). The executable is located at `/data/adb/ksu/bin/busybox`. KernelSU's BusyBox supports runtime toggle-able "ASH Standalone Shell Mode". What this Standalone Mode means is that when running in the `ash` shell of BusyBox, every single command will directly use the applet within BusyBox, regardless of what is set as `PATH`. For example, commands like `ls`, `rm`, `chmod` will **NOT** use what is in `PATH` (in the case of Android by default it will be `/system/bin/ls`, `/system/bin/rm`, and `/system/bin/chmod` respectively), but will instead directly call internal BusyBox applets. This makes sure that scripts always run in a predictable environment and always have the full suite of commands no matter which Android version it is running on. To force a command *not* to use BusyBox, you have to call the executable with full paths.

Every single shell script running in the context of KernelSU will be executed in BusyBox's `ash` shell with Standalone Mode enabled. For what is relevant to 3rd party developers, this includes all boot scripts and module installation scripts.

For those who want to use this Standalone Mode feature outside of KernelSU, there are 2 ways to enable it:

1. Set environment variable `ASH_STANDALONE` to `1` Example: `ASH_STANDALONE=1 /data/adb/ksu/bin/busybox sh <script>`
2. Toggle with command-line options:`/data/adb/ksu/bin/busybox sh -o standalone <script>`

To make sure all subsequent `sh` shell executed also runs in Standalone Mode, option 1 is the preferred method (and this is what KernelSU and the KernelSU manager use internally) as environment variables are inherited down to child processes.

::: tip DIFFERENCE WITH MAGISK
KernelSU's BusyBox is now using the binary file compiled directly from the Magisk project. **Thanks to Magisk!** Therefore, you don't need to worry about compatibility issues between BusyBox scripts in Magisk and KernelSU, as they're exactly the same!
:::

## KernelSU modules

A KernelSU module is a folder placed in `/data/adb/modules` with the structure below:

```txt
/data/adb/modules
├── .
├── .
|
├── $MODID                  <--- The folder is named with the ID of the module
│   │
│   │      *** Module Identity ***
│   │
│   ├── module.prop         <--- This file stores the metadata of the module
│   │
│   │      *** Main Contents ***
│   │
│   ├── system              <--- This folder will be mounted if skip_mount does not exist
│   │   ├── ...
│   │   ├── ...
│   │   └── ...
│   │
│   │      *** Status Flags ***
│   │
│   ├── skip_mount          <--- If exists, KernelSU will NOT mount your system folder
│   ├── disable             <--- If exists, the module will be disabled
│   ├── remove              <--- If exists, the module will be removed next reboot
│   │
│   │      *** Optional Files ***
│   │
│   ├── post-fs-data.sh     <--- This script will be executed in post-fs-data
│   ├── post-mount.sh       <--- This script will be executed in post-mount
│   ├── service.sh          <--- This script will be executed in late_start service
│   ├── boot-completed.sh   <--- This script will be executed on boot completed
|   ├── uninstall.sh        <--- This script will be executed when KernelSU removes your module
|   ├── action.sh           <--- This script will be executed when user click the Action button in KernelSU app
│   ├── system.prop         <--- Properties in this file will be loaded as system properties by resetprop
│   ├── sepolicy.rule       <--- Additional custom sepolicy rules
│   ├── initrc/             <--- .rc files in this directory will be injected into init.rc on boot
│   │   ├── myservice.rc
│   │   └── ...
│   ├── preinit/            <--- preinit.conf and its files, run before SELinux policy is loaded
│   │
│   │      *** Auto Generated, DO NOT MANUALLY CREATE OR MODIFY ***
│   │
│   ├── vendor              <--- A symlink to $MODID/system/vendor
│   ├── product             <--- A symlink to $MODID/system/product
│   ├── system_ext          <--- A symlink to $MODID/system/system_ext
│   │
│   │      *** Any additional files / folders are allowed ***
│   │
│   ├── ...
│   └── ...
|
├── another_module
│   ├── .
│   └── .
├── .
├── .
```

::: tip DIFFERENCE WITH MAGISK
KernelSU doesn't have built-in support for Zygisk, so there is no content related to Zygisk in the module. However, you can use [ZygiskNext](https://github.com/Dr-TSNG/ZygiskNext) to support Zygisk modules. In this case, the content of the Zygisk module is identical to that supported by Magisk.
:::

### module.prop

`module.prop` is a configuration file for a module. In KernelSU, if a module doesn't contain this file, it won't be recognized as a module. The format of this file is as follows:

```txt
id=<string>
name=<string>
version=<string>
versionCode=<int>
author=<string>
description=<string>
updateJson=<url> (optional)
actionIcon=<path> (optional)
webuiIcon=<path> (optional)
```

* `id` has to match this regular expression: `^[a-zA-Z][a-zA-Z0-9._-]+$`
  Example: ✓ `a_module`, ✓ `a.module`, ✓ `module-101`, ✗ `a module`, ✗ `1_module`, ✗ `-a-module`
  This is the **unique identifier** of your module. You should not change it once published.
* `versionCode` has to be an **integer**. This is used to compare versions.
* Others that were not mentioned above can be any **single line** string.
* Make sure to use the `UNIX (LF)` line break type and not the `Windows (CR+LF)` or `Macintosh (CR)`.
* `actionIcon` and `webuiIcon` are optional icon paths used as the default
  icons for the module action shortcut and WebUI shortcut in the Manager. These
  paths must be relative to the module root directory. For example,
  `actionIcon=icon/icon.png` will be resolved as `<MODDIR>/icon/icon.png`.

::: tip DYNAMIC DESCRIPTION
The `description` field can be dynamically overridden at runtime using the module configuration system. See [Overriding Module Description](module-config.md#overriding-module-description) for details.
:::

### Shell scripts

Please read the [Boot scripts](#boot-scripts) section to understand the difference between `post-fs-data.sh` and `service.sh`. For most module developers, `service.sh` should be good enough if you just need to run a boot script, if you need to run the script after boot completed, please use `boot-completed.sh`. If you want to do something after mounting OverlayFS, please use `post-mount.sh`.

In all scripts of your module, please use `MODDIR=${0%/*}` to get your module's base directory path; do **NOT** hardcode your module path in scripts.

::: tip DIFFERENCE WITH MAGISK
You can use the environment variable `KSU` to determine if a script is running in KernelSU or Magisk. If running in KernelSU, this value will be set to `true`.
:::

### `system` directory

The contents of this directory will be overlaid on top of the system's `/system` partition after the system is booted. This means that:

::: tip METAMODULE REQUIREMENT
The `system` directory is only mounted if you have a metamodule installed that provides mounting functionality (such as `meta-overlayfs`). The metamodule handles how modules are mounted. See the [Metamodule Guide](metamodule.md) for more information.
:::

1. Files with the same name as those in the corresponding directory in the system will be overwritten by the files in this directory.
2. Folders with the same name as those in the corresponding directory in the system will be merged with the folders in this directory.

If you want to delete a file or folder in the original system directory, you need to create a file with the same name as the file/folder in the module directory using `mknod filename c 0 0`. This way, the OverlayFS system will automatically "whiteout" this file as if it has been deleted (the /system partition isn't actually changed).

You can also declare a variable named `REMOVE` containing a list of directories in `customize.sh` to execute removal operations, and KernelSU will automatically execute `mknod <TARGET> c 0 0` in the corresponding directories of the module. For example:

```sh
REMOVE="
/system/app/YouTube
/system/app/Bloatware
"
```

The above list will execute `mknod $MODPATH/system/app/YouTube c 0 0` and `mknod $MODPATH/system/app/Bloatware c 0 0`, `/system/app/YouTube` and `/system/app/Bloatware` will be removed after the module takes effect.

If you want to replace a directory in the system, you need to create a directory with the same path in your module directory, and then set the attribute `setfattr -n trusted.overlay.opaque -v y <TARGET>` for this directory. This way, the OverlayFS system will automatically replace the corresponding directory in the system (without changing the /system partition).

You can declare a variable named `REPLACE` in your `customize.sh` file, which includes a list of directories to be replaced, and KernelSU will automatically perform the corresponding operations in your module directory. For example:

```sh
REPLACE="
/system/app/YouTube
/system/app/Bloatware
"
```

This list will automatically create the directories `$MODPATH/system/app/YouTube` and `$MODPATH/system/app/Bloatware`, and then execute `setfattr -n trusted.overlay.opaque -v y $MODPATH/system/app/YouTube` and `setfattr -n trusted.overlay.opaque -v y $MODPATH/system/app/Bloatware`. After the module takes effect, `/system/app/YouTube` and `/system/app/Bloatware` will be replaced with empty directories.

::: tip DIFFERENCE WITH MAGISK
KernelSU uses a [metamodule architecture](metamodule.md) where mounting is delegated to pluggable metamodules. The official `meta-overlayfs` metamodule uses the kernel's OverlayFS for systemless modifications, while Magisk uses magic mount (bind mount) built directly into its core. Both achieve the same goal: modifying `/system` files without physically modifying the `/system` partition. KernelSU's approach provides more flexibility and reduces detection surface.
:::

If you're interested in OverlayFS, it's recommended to read the Linux Kernel's [documentation on OverlayFS](https://docs.kernel.org/filesystems/overlayfs.html). For details on KernelSU's metamodule system, see the [Metamodule Guide](metamodule.md).

### system.prop

This file follows the same format as `build.prop`. Each line comprises of `[key]=[value]`.

### sepolicy.rule

If your module requires some additional sepolicy patches, please add those rules into this file. Each line in this file will be treated as a policy statement.

### initrc Injection {#initrc-injection}

KernelSU provides a mechanism to inject custom Android Init RC directives into the system's `init.rc`. This allows modules to register custom Android services, set property triggers, or perform other Init language actions without modifying the system partition.

During boot, the KernelSU kernel module intercepts `read()` and `fstat()` system calls. When the Android init process reads `/system/etc/init/hw/init.rc`, KernelSU transparently appends the custom RC content to the end of the file. The init process parses these injected directives just like the original init.rc content.

On the userspace side, ksud concatenates all `.rc` files from enabled modules into a single `modules.rc` file, stored on the `/metadata` partition. This file is automatically regenerated whenever a module's state changes (install, enable, disable, uninstall, etc.).

#### Module initrc Files

Create an `initrc/` subdirectory in your module directory and place your `.rc` files there:

```txt
/data/adb/modules/<MODID>/
├── initrc/
│   ├── myservice.rc
│   └── another.rc
└── ...
```

::: tip

* Files must have an `.rc` extension.
* As long as the module is enabled, all `.rc` files in the `initrc/` directory will be included (executable permission is not required).
* Files are processed in **alphabetical order of file name** within the directory, and modules are processed in **alphabetical order of module ID**.
  :::

#### General initrc Files

In addition to module-level RC files, you can place `.rc` files in the global directory:

```txt
/data/adb/initrc.d/
├── myservice.rc
└── another.rc
```

::: warning General initrc files require executable permission
Unlike the module `initrc/` directory, files in `/data/adb/initrc.d/` **must have executable permissions** to be included. Non-executable `.rc` files will be silently skipped.
:::

General `initrc.d/` files are processed before any module RC files.

#### Example

Here is an example `.rc` file that registers a custom Android service:

```rc
service myservice /data/adb/modules/mymodule/bin/myservice
    user root
    group root
    disabled
    seclabel u:r:ksu:s0

on property:sys.boot_completed=1
    start myservice
```

If this file is placed at `/data/adb/modules/mymodule/initrc/myservice.rc`, it will register a service named `myservice` at boot and start it when `sys.boot_completed=1` is reached.

#### Manual Refresh

You can manually trigger the regeneration of `modules.rc` with the following command (changes take effect on the next boot):

```sh
ksud initrc refresh
```

::: tip

* initrc injection happens extremely early in the boot process (when init reads init.rc), **before** post-fs-data and any module scripts are executed.
* The injected RC content is treated by init as part of the original init.rc, supporting all Android Init language syntax (service definitions, triggers, property settings, etc.).
* initrc injection is **not available** in **late-load mode**, as the system call hooks are not installed in that mode.
* Module RC injection can be disabled by passing the `--no-custom-rc` parameter when patching the image with ksud.
  :::

### preinit {#preinit}

A module can run a few actions **before the SELinux policy is loaded and before init.rc is parsed**, for example to load a kernel module, run a static program or write procfs/sysfs. Put them in a `preinit/` directory described by `preinit/preinit.conf`:

```txt
/data/adb/modules/<MODID>/
├── preinit/
│   ├── preinit.conf
│   ├── kmod/foo.ko
│   └── bin/fixup
└── ...
```

ksud copies the whole `preinit/` directory of every enabled module to the `/metadata` partition. On the next boot a runner embedded in the KernelSU kernel module executes the actions when first stage init hands over to `/system/bin/init selinux_setup`. At this point `/metadata`, `/system` and `/vendor` are mounted and `/proc`, `/sys` and `/dev` are available, but there is no policy, no property service and no ueventd yet. Modules are processed in alphabetical order of module ID.

#### preinit.conf

One action per line. Lines starting with `#` and empty lines are ignored. Arguments are separated by blanks; use double quotes to keep blanks in an argument and `\"` / `\\` for a literal quote or backslash. Variables are **not** expanded. Relative paths are relative to the module's preinit directory. Lines are at most 4 KB and the file at most 64 KB.

| Action | Syntax | Meaning |
|---|---|---|
| `insmod` | `insmod <ko> [params...]` | Load a kernel module. Undefined symbols are looked up in the kernel's kallsyms and bound before `init_module`, so the module may use non-exported symbols; a module that needs no such binding is loaded with `finit_module`. Parameters are joined with spaces |
| `exec` | `exec [-t <ms>] <prog> [args...]` | Run a program and wait for it. A non-zero exit status is a failure. It is killed after 5000 ms by default |
| `write` | `write <path> <content>` | Write `content` to an existing file, without a trailing newline. Meant for procfs/sysfs |

Append `?` to an action to ignore its failure. Any other failing action stops the rest of **this** module; the next module still runs.

```txt
# preinit/preinit.conf
insmod   kmod/foo.ko  debug=1 name="a b"
insmod?  kmod/optional.ko
exec -t 10000  bin/fixup --flag
write    /proc/sys/kernel/printk_devkmsg on
```

Programs started by `exec` run as root with full capabilities and no SELinux restriction. Their working directory is the module's preinit directory, stdin is `/dev/null` and stdout/stderr are forwarded to the kernel log line by line with a `ksu_preinit[<MODID>]:` prefix. The environment contains only `KSU=true`, `KSU_PREINIT=1`, `MODID`, `PREINIT_DIR` and `PATH=/system/bin:/vendor/bin`.

::: warning LIMITATIONS

* There is no shell: only **statically linked** programs work, `/apex` is not mounted yet.
* `/dev` only contains the nodes created by first stage init; ueventd is not running.
* The firmware loader has no userspace fallback, firmware must be loadable directly from `/vendor/firmware` and similar paths.
* Properties can neither be read nor set.
* Only the vendor modules of first stage are loaded. A `.ko` that depends on symbols from vendor_dlkm (loaded in second stage) fails to load.
* Nothing may keep running: all processes left over when preinit ends are killed. The whole preinit stage is limited to 30 seconds.
* Changes take effect on the **next boot**; a hot install does not run preinit.
* The directory and `preinit.conf` (and the `.ko` / program files used) must be owned by root and not writable by group or others. ksud takes care of this when copying.
* ksud keeps the permission bits of each file (minus group/other write). The installer sets module files to `0644`, so programs run by `exec` need `set_perm $MODPATH/preinit/bin/xxx 0 0 0755` in `customize.sh`.
  :::

#### Space on /metadata

`/metadata` is small (often around 10 MB) and also holds encryption keys and OTA state. ksud therefore keeps at least 8 MB free and uses at most 4 MB for preinit. Modules are copied in alphabetical order of module ID; a module that does not fit is skipped entirely, and the installer prints a warning. Check the result with:

```sh
ksud module preinit status
```

#### Bootloop protection

Before running any action the runner increments a boot counter, which ksud clears when boot completes. After 3 boots in a row that did not complete, preinit is skipped and stays disabled until the preinit content of a module changes or you run:

```sh
ksud module preinit reset
```

preinit is also not run in charger mode, and safe mode disables all modules, which removes their preinit content for the next boot. To get out of a bootloop by hand, delete `/metadata/vold/ksu/preinit` (or `/metadata/watchdog/ksu/preinit`, `/metadata/ksu/preinit`, whichever exists) from recovery, or `fastboot boot` the stock image and delete it there.

::: tip

* preinit is **not available** in **late-load mode** and on devices that do not use a two stage init (devices launched with system-as-root before Android 10).
* When the running kernel does not run preinit (late-load mode, `preinit=0`, or a kernel built without it), ksud copies nothing to `/metadata` and `ksud module preinit status` reports the modules as `unsupported`.
* preinit can be disabled entirely with the `preinit=0` parameter of the KernelSU kernel module, which `ksud boot-patch` / `boot-patch-v2` set with `--no-preinit`.
* Anyone who can write to `/metadata` can run code this early. Only install modules you trust.
  :::

## Module installer

A KernelSU module installer is a KernelSU module packaged in a ZIP file that can be flashed in the KernelSU manager. The simplest KernelSU module installer is just a KernelSU module packed as a ZIP file.

```txt
module.zip
│
├── customize.sh                       <--- (Optional, more details later)
│                                           This script will be sourced by update-binary
├── ...
├── ...  /* The rest of module's files */
│
```

::: warning
KernelSU module is **NOT** compatible for installation in a custom Recovery!
:::

### Customization

If you need to customize the module installation process, optionally you can create a script in the installer named `customize.sh`. This script will be **sourced** (not executed) by the module installer script after all files are extracted and default permissions and secontext are applied. This is very useful if your module requires additional setup based on the device ABI, or you need to set special permissions/secontext for some of your module files.

If you would like to fully control and customize the installation process, declare `SKIPUNZIP=1` in `customize.sh` to skip all default installation steps. By doing so, your `customize.sh` will be responsible to install everything by itself.

The `customize.sh` script runs in KernelSU's BusyBox `ash` shell with Standalone Mode enabled. The following variables and functions are available:

#### Variables

* `KSU` (bool): a variable to mark that the script is running in the KernelSU environment, and the value of this variable will always be true. You can use it to distinguish between KernelSU and Magisk.
* `KSU_VER` (string): the version string of currently installed KernelSU (e.g. `v0.4.0`).
* `KSU_VER_CODE` (int): the version code of currently installed KernelSU in userspace (e.g. `10672`).
* `KSU_KERNEL_VER_CODE` (int): the version code of currently installed KernelSU in kernel space (e.g. `10672`).
* `BOOTMODE` (bool): always be `true` in KernelSU.
* `MODPATH` (path): the path where your module files should be installed.
* `TMPDIR` (path): a place where you can temporarily store files.
* `ZIPFILE` (path): your module's installation ZIP.
* `ARCH` (string): the CPU architecture of the device. Value is either `arm`, `arm64`, `x86`, or `x64`.
* `IS64BIT` (bool): `true` if `$ARCH` is either `arm64` or `x64`.
* `API` (int): the API level (Android version) of the device (e.g., `23` for Android 6.0).
* `KSU_UAPI_VER` (int): the UAPI version of KernelSU userspace (ksud) (e.g., `2`). This version is incremented when there are breaking changes in the kernel driver, and can be used by modules to check compatibility.
* `KSU_RUNTIME_MODE` (string): the current KernelSU runtime mode. Possible values are `built-in` (a.k.a. GKI mode, compiled into the kernel), `lkm` (loaded as a kernel module at boot), or `late-load` (loaded as a kernel module after boot).
* `KSU_LATE_LOAD` (int?): if KernelSU is late-loaded after boot, this variable is set to `1`; otherwise it is not set.
* `KSU_HAS_HOT_INSTALL` (bool): `true` if the installer supports [hot install](#hot-install).

::: warning
In KernelSU, `MAGISK_VER_CODE` is always `25200`, and `MAGISK_VER` is always `v25.2`. Please don't use these two variables to determine whether KernelSU is running or not.
:::

#### Functions

```txt
ui_print <msg>
    print <msg> to console
    Avoid using 'echo' as it will not display in custom recovery's console

abort <msg>
    print error message <msg> to console and terminate the installation
    Avoid using 'exit' as it will skip the termination cleanup steps

set_perm <target> <owner> <group> <permission> [context]
    if [context] is not set, the default is "u:object_r:system_file:s0"
    this function is a shorthand for the following commands:
       chown owner.group target
       chmod permission target
       chcon context target

set_perm_recursive <directory> <owner> <group> <dirpermission> <filepermission> [context]
    if [context] is not set, the default is "u:object_r:system_file:s0"
    for all files in <directory>, it will call:
       set_perm file owner group filepermission context
    for all directories in <directory> (including itself), it will call:
       set_perm dir owner group dirpermission context
```

### Hot install {#hot-install}

By default, a newly installed or updated module is staged in `/data/adb/modules_update` and only takes effect after a reboot. If your module does not depend on anything that only happens at boot, `customize.sh` can ask KernelSU to apply it right away:

```sh
# customize.sh
export MODULE_HOT_INSTALL_REQUEST=true
# Optional: a script in your module to run once the new version is live
export MODULE_HOT_RUN_SCRIPT=hotinstall.sh
```

The request is evaluated at install time, so the module decides whether a reboot is needed, for example by only requesting a hot install when the new version does not change its `system` files.

Once the installer (including `customize.sh` and a metamodule's `metainstall.sh`) finishes successfully, KernelSU:

1. Replaces `/data/adb/modules/<id>` with the new version. The old directory is moved aside and only deleted on the next boot, so processes and mounts still using the old version keep working until then.
2. Loads the module's `sepolicy.rule`. Rules are only ever added, so rules dropped by the new version stay active until the next reboot.
3. Runs `MODULE_HOT_RUN_SCRIPT`, a path relative to the module directory, the same way as boot scripts: in the global mount namespace, with the script's directory as the working directory. Its output is shown in the installer, stdin is `/dev/null` and `SIGPIPE` is ignored, so background processes are not killed when the installer goes away. The installer waits up to 30 seconds for it; start long-running services in the background. If the script does not exist it is skipped.
4. Leaves no `update` flag behind, so the manager does not ask for a reboot.

The module stays hot installed even if loading `sepolicy.rule` fails or the script fails or times out; check the installer output.

Like any install, this enables the module even if the previous version was disabled.

Effective immediately: the module files themselves, WebUI, `action.sh` and the information in `module.prop`.

Only applied on the next boot: files under `system` and the other partition directories (mounted by the metamodule), `system.prop`, `initrc`, `preinit`, and the `post-fs-data.sh`, `post-mount.sh`, `service.sh` and `boot-completed.sh` stages. They are **not** run during a hot install, but keep running on every boot as usual. If you need their effect now, do it in `MODULE_HOT_RUN_SCRIPT`, e.g. by starting your service or calling `resetprop`.

The old version is not stopped for you, and `uninstall.sh` is not run on updates. If your module runs a daemon, `MODULE_HOT_RUN_SCRIPT` should stop the old instance before starting the new one.

KernelSU falls back to applying the module on the next reboot, and prints the reason, when:

* The module being installed is a metamodule.
* The device is in safe mode.
* `MODULE_HOT_RUN_SCRIPT` is not a relative path to a file inside the module (symlinks pointing outside the module are rejected).
* The module directory cannot be replaced.

## Boot scripts

In KernelSU, scripts are divided into two types based on their running mode: post-fs-data mode and late_start service mode.

* post-fs-data mode
  * This stage is BLOCKING. The boot process is paused before execution is done or after 10 seconds.
  * Scripts run before any modules are mounted. This allows a module developer to dynamically adjust their modules before it gets mounted.
  * This stage happens before Zygote is started, which pretty much means everything in Android.
  * **WARNING:** Using `setprop` will deadlock the boot process! Please use `resetprop -n <prop_name> <prop_value>` instead.
  * **Only run scripts in this mode if necessary**.
* late_start service mode
  * This stage is NON-BLOCKING. Your script runs in parallel with the rest of the booting process.
  * **This is the recommended stage to run most scripts**.

In KernelSU, startup scripts are divided into two types based on their storage location: general scripts and module scripts.

* General scripts
  * Placed in `/data/adb/post-fs-data.d`, `/data/adb/service.d`, `/data/adb/post-mount.d` or `/data/adb/boot-completed.d`.
  * Only executed if the script is set as executable (`chmod +x script.sh`).
  * Scripts in `post-fs-data.d` runs in post-fs-data mode, and scripts in `service.d` runs in late_start service mode.
  * Modules should **NOT** add general scripts during installation.
* Module scripts
  * Placed in the module's own folder.
  * Only executed if the module is enabled.
  * `post-fs-data.sh` runs in post-fs-data mode, `service.sh` runs in late_start service mode, `boot-completed.sh` runs on boot completed, `post-mount.sh` runs on OverlayFS mounted.

All boot scripts will run in KernelSU's BusyBox `ash` shell with Standalone Mode enabled.

### Boot scripts process explanation

The following is the relevant boot process for Android (some parts are omitted), which includes the operation of KernelSU (with leading asterisks), and can help you better understand the purpose of these module scripts:

```txt
0. Bootloader (nothing on screen)
load patched boot.img
load kernel:
    - GKI mode: GKI kernel with KernelSU integrated
    - LKM mode: stock kernel
...

1. kernel exec init (OEM logo on screen):
    - GKI mode: stock init
    - LKM mode: exec ksuinit, insmod kernelsu.ko, exec stock init
mount /dev, /dev/pts, /proc, /sys, etc.
  *preinit: before loading SELinux policy, the kernel runs modules' preinit.conf
property-init -> read default props
read init.rc
  *initrc injection: Kernel hook appends KernelSU core RC and module modules.rc to init.rc
...
early-init -> init -> late_init
early-fs
   start vold
fs
  mount /vendor, /system, /persist, etc.
post-fs-data
  *safe mode check
  *execute general scripts in post-fs-data.d/
  *load sepolicy.rule
  *execute metamodule's post-fs-data.sh (if exists)
  *execute module scripts post-fs-data.sh
    **(Zygisk)./bin/zygisk-ptrace64 monitor
  *(pre)load system.prop (same as resetprop -n)
  *execute metamodule's metamount.sh (mounts all modules)
  *execute general scripts in post-mount.d/
  *execute metamodule's post-mount.sh (if exists)
  *execute module scripts post-mount.sh
zygote-start
load_all_props_action
  *execute resetprop (actual set props for resetprop with -n option)
... -> boot
  class_start core
    start-service logd, console, vold, etc.
  class_start main
    start-service adb, netd (iptables), zygote, etc.

2. kernel2user init (ROM animation on screen, start by service bootanim)
*execute general scripts in service.d/
*execute metamodule's service.sh (if exists)
*execute module scripts service.sh
*set props for resetprop without -p option
  **(Zygisk) hook zygote (start zygiskd)
  **(Zygisk) mount zygisksu/module.prop
start system apps (autostart)
...
boot complete (broadcast ACTION_BOOT_COMPLETED event)
*execute general scripts in boot-completed.d/
*execute metamodule's boot-completed.sh (if exists)
*execute module scripts boot-completed.sh

3. User operable (lock screen)
input password to decrypt /data/data
*actual set props for resetprop with -p option
start user apps (autostart)
```

If you're interested in Android Init Language, it's recommended to read its [documentation](https://android.googlesource.com/platform/system/core/+/master/init/README.md).

## Late-load mode {#late-load-mode}

In addition to the standard boot flow described above, KernelSU supports a **late-load mode** for LKM (Loadable Kernel Module) scenarios. In this mode, the KernelSU kernel module is loaded **after the system has fully booted**, rather than during the init process.

### When does late-load happen?

Late-load is triggered by running the `ksud late-load` command. This command:

1. Detects the current KMI version and loads the corresponding `kernelsu.ko` from embedded assets.
2. Performs module initialization (SELinux rules, allowlist, features, etc.) that would normally happen during boot.

Since the system is already fully running, certain boot-time mechanisms are unavailable or unnecessary.

### Differences from standard boot

| Behavior | Standard boot | Late-load mode |
|----------|:---:|:---:|
| Kernel module loaded by init (PID 1) | Yes | No (loaded after boot) |
| initrc injection (module `.rc` files to init.rc) | Yes | Unavailable |
| preinit (module `preinit/preinit.conf`) | Yes | Unavailable |
| kprobe hooks for ksud (execve/read/fstat/input) | Yes | Skipped |
| Safe mode detection (volume key) | Yes | Always disabled |
| Boot log capture (logcat/dmesg) | Yes | Skipped |
| Magisk coexistence check | Yes | Skipped |
| `post-fs-data` event reported to kernel | Yes | Skipped |
| `boot-completed` event reported to kernel | Yes | Set directly during init |
| `post-fs-data.sh` / `post-fs-data.d/` scripts | Yes | Replaced by `late-load` stage |
| `system.prop` loading | Yes | Yes |
| OverlayFS mount (metamodule) | Yes | Yes |
| `post-mount.sh` / `post-mount.d/` scripts | Yes | Yes |
| `service.sh` / `service.d/` scripts | Yes | Yes |
| `boot-completed.sh` / `boot-completed.d/` scripts | Yes | Yes |
| `KSU_LATE_LOAD` environment variable | Not set | Set to `1` |
| Kernel info flag `0x4` | Not set | Set |

### Script execution order

In late-load mode, the script execution order is:

```txt
ksud late-load:
  1. Load kernelsu.ko (if not already loaded)
  2. Extract binaries, handle module updates, load SELinux rules, init features
  3. Execute late-load.d/ and module late-load scripts (blocking)
  4. Load system.prop (resetprop -n)
  5. Execute metamodule mount script (OverlayFS)
  6. Execute post-mount.d/ and module post-mount.sh (blocking)
  7. Execute service.d/ and module service.sh (non-blocking)
  8. Execute boot-completed.d/ and module boot-completed.sh (non-blocking)
```

### Late-load specific scripts

Modules can provide a `late-load.sh` script that runs **only** in late-load mode, as a replacement for `post-fs-data.sh`. This script runs before OverlayFS mounting, similar to `post-fs-data.sh` in the standard flow.

Additionally, general scripts can be placed in `/data/adb/late-load.d/` to run during this stage.

### Detecting late-load mode in scripts

Modules can detect late-load mode by checking the `KSU_LATE_LOAD` environment variable:

```sh
if [ "$KSU_LATE_LOAD" = "1" ]; then
    # Running in late-load mode
    echo "Late-load mode detected"
fi
```

This allows modules to adjust their behavior accordingly, for example skipping operations that are only needed during early boot.
