Skip to content

Add module preinit: run actions before SELinux policy loads - #3907

Merged
Ylarod merged 14 commits into
mainfrom
preinit
Oct 16, 2026
Merged

Ylarod merged 14 commits into
mainfrom
preinit

Conversation

@Ylarod

@Ylarod Ylarod commented Oct 16, 2026

Copy link
Copy Markdown
Collaborator

What

Adds a preinit mechanism that lets a module run a few actions before the SELinux policy is loaded and before init.rc is parsed — e.g. load a kernel module, run a static program, or write to procfs/sysfs. A module describes them in a preinit/ directory:

mymodule/
└── preinit/
    ├── preinit.conf      # one action per line: insmod / exec / write
    └── ...               # .ko files, programs, ...

How it works

  • Kernel runner: a static, nolibc-built runner is embedded in the kernel module (.incbin) and staged in a shmem file at boot. When PID 1 execs /system/bin/init selinux_setup, the execve hook swaps the runner in via /proc/self/fd/N. The runner parses each module's preinit.conf, performs the actions, then execs the real init. A boot counter disables preinit after 3 failed boots in a row.
  • ksud syncs each enabled module's preinit/ dir to the preinit partition (manifest-based, incremental, within a space budget on the small /metadata). Storage dir is /metadata/vold/ksu when available (hidden, does not change /metadata's link count), else /metadata/watchdog/ksu, else /metadata/ksu. ksud module preinit status|reset inspects per-module state and clears the boot counter.
  • boot-patch / boot-patch-v2 gain --no-preinit (sets the preinit=0 kernel parameter).
  • Manager shows a warning tag on modules whose preinit content was not applied (no space / error / bootloop).
  • Docs: preinit.conf format, actions, space budget, bootloop recovery (EN + zh_CN).

Testing

  • Verified on a Redmi K80 (android14-6.1): runner runs before policy load (process context kernel), /metadata write + fsync survives reboot, bootloop counter parks and reset recovers, all conf actions (insmod/exec/write, optional ?, quoting, exit codes, output forwarding). Confirmed with both v1 (ksuinit in init_boot) and v2 (ko injected into boot). --no-preinit disables it (preinit: disabled). Charger mode (androidboot.mode=charger) is skipped and does not bump the counter. Data lands under /metadata/vold/ksu with the vold_metadata_file label; /metadata root link count is unchanged.
  • Builds pass for all 8 KMIs (android12-5.10 … android17-6.18).

Ylarod added 14 commits October 16, 2026 04:43
Embed a static nolibc runner into the kernel module and stage it in a
shmem file at boot. When PID 1 execs "/system/bin/init selinux_setup",
swap the runner in via /proc/self/fd/N so it runs before the SELinux
policy is loaded and before init.rc is parsed. The runner parses
preinit.conf from each module synced under /metadata and performs
insmod/exec/write actions, with a boot counter that disables preinit
after repeated failed boots.

The runner source (preinit/runner/) vendors nolibc from Linux v6.18 and
is built by a dedicated Kbuild rule, then linked in via .incbin. preinit
can be turned off with the preinit=0 module parameter. Move
userspace_stack_buffer to util.h so the execve hook can reuse it.

The preinit data lives under /metadata/vold/ksu when available, else
/metadata/watchdog/ksu, else /metadata/ksu.
Copy each enabled module's preinit/ directory to the preinit partition so
the kernel runner can execute it on the next boot. Pick /metadata/vold/ksu
when /metadata/vold exists (hidden, does not change /metadata's link
count), else /metadata/watchdog/ksu, else /metadata/ksu, and label the
files to match the host dir. Sync is manifest-based and incremental, stays
within a space budget on the small /metadata partition, cleans leftovers
from the other candidate dirs, and is owned by root.

Add `ksud module preinit status|reset` to inspect the per-module state
(ok/no_space/error/bootloop) and clear the boot counter, regenerate the
preinit content on module changes alongside modules.rc, and add
`--no-preinit` to boot-patch / boot-patch-v2 to set the preinit=0 kernel
parameter.
Parse the "preinit" field from `ksud module list` and show a warning tag
on modules whose preinit content was not applied (no_space/error/
bootloop), so users notice when a module's preinit actions will not run.
Describe the module preinit/ directory and preinit.conf, the actions,
the space budget on /metadata, the bootloop protection, and how to
recover, in both English and Simplified Chinese.
Add KSU_IOCTL_INSMOD (UAPI v6): load a kernel module from a caller-provided
binary image, resolving its undefined (non-exported) symbols against kallsyms
before handing it to the kernel's internal load_module(). This lets a module
reference non-exported symbols, acting as a self-contained replacement for an
external LKM loader.

ksud and the preinit runner prefer this supercall when supported and fall back
to the userspace loader / finit_module otherwise.
Follow ksuinit's approach: after binding the undefined symbols against
kallsyms, stage the patched image in an anonymous mapping of the caller and
load it with the regular init_module syscall instead of calling the static
load_module() resolved by name. CAP_SYS_MODULE, modules_disabled, LSM and
signature checks apply again, LTO renaming of load_module is no longer an
issue, and the build no longer depends on the private struct load_info.

Validate the ELF header and bound every section and string offset taken
from the image before patching it, the previous checks could overflow and
walk past the buffer. Take the symbol strtab from the symtab's sh_link.

Also move a const to the top of debug::insmod to satisfy clippy.
After killing a timed out worker, wait for it for at most a second: a
worker stuck in a module's init cannot be killed, and blocking there would
hang the boot. When reaping orphans, only collect what has already exited
instead of polling for a second, first stage init may have long-lived
children of its own (snapuserd).

Only fall back from the KSU insmod supercall to finit_module when the
supercall is unusable (ENOTTY/EPERM/ENOEXEC), so a module whose init failed
is not loaded a second time. Treat EEXIST from the supercall as success.
- Remove /metadata/vold/ksu on uninstall, it was left behind.
- Key the preinit manifest on ctime instead of mtime: extraction keeps the
  zip mtime, so a same-size content change could go unnoticed.
- Docs: list /metadata/vold/ksu/preinit in the recovery steps and describe
  insmod's kallsyms resolution with the finit_module fallback.
Kernels before UAPI 6 only look for modules.rc under /metadata/watchdog
and /metadata. A new ksud on such a kernel moved modules.rc to
/metadata/vold and deleted the old copies, silently dropping the modules'
init rc. Keep preferring /metadata/vold, but only when the running kernel
reports UAPI 6 or later, otherwise keep the previous watchdog/default
choice.
KSU_IOCTL_INSMOD parsed and patched the module ELF inside the kernel. Drop
it for KSU_IOCTL_LOOKUP_SYMBOLS (same number, UAPI v6 is unreleased): a
generic batch lookup that resolves up to KSU_LOOKUP_SYMBOLS_MAX kernel
symbols, exported or not, per call and returns the number found. Missing,
empty or overlong names report address 0 instead of failing the batch.

Callers now bind a module's undefined symbols in userspace and load it with
init_module themselves:

- preinit runner: maps the validated module privately, binds its undefined
  symbols in batches of 256 and loads it with init_module; a module that
  needs no binding still goes through finit_module on the fd.
- ksuinit: add load_module_with_resolver, sharing symbol collection,
  binding and the vermagic retry with the kallsyms based load_module.
- ksud: add ksucalls::lookup_symbols; debug insmod uses it on UAPI v6+ and
  falls back to /proc/kallsyms on older kernels.
Match kernel symbols the way ksuinit's /proc/kallsyms walk does, so a
module binds to the same addresses whichever loader is used: a kallsyms
name is cut at its first '$', else at ".llvm.", the first match in
kallsyms order wins, and module symbols are never returned. The previous
exact lookup missed ".llvm." suffixed names on 6.12+, where the kernel no
longer strips them, and could pick a later duplicate.

6.1+ kernels index kallsyms by name, so names are still looked up one by
one there, keeping the first match. What that misses, and every name on
5.10/5.15 where a lookup by name is a linear scan, is resolved by a single
kallsyms walk against the sorted batch instead of one scan per name.

Names are copied in up front with strndup_user; an empty or overlong name
still reports address 0.
… files

Add KSU_GET_INFO_FLAG_PREINIT, set when the runner is built in, not
disabled with preinit=0 and KernelSU was not late loaded. Without it ksud
copies nothing to /metadata, removes what an earlier sync left there and
reports the modules as "unsupported", which the manager now shows.

The manifest tracks ctime, so a module script that chmods or chcons its
preinit files on every boot made each post-fs-data look like new content
and cleared the boot counter, defeating the bootloop protection. When the
manifest differs, compare against the existing copy first and leave both
the copy and the counter alone if the content is the same.

Remove modules that are gone before copying anything, and before
installing a changed module check the space actually free: the budget
counts the old copy as free while install writes the new copy first, so
drop the old copy when both would cut into the reserve.

Docs: move preinit/ out of the initrc/ subtree in the module layout,
describe the two stage init limitation precisely and the unsupported
state.
- After an exec action times out, wait for the killed child for at most
  a second: a child stuck in D state ignores SIGKILL and blocked the
  worker until the global deadline. PID 1 kills and reaps what is left.
- Exec the program through the fd that passed the ownership checks
  (execveat with AT_EMPTY_PATH) instead of resolving the path again.
- The runner includes uapi/supercall.h, list the UAPI headers as
  prerequisites so an ioctl change rebuilds the embedded runner.
Pick the modules that fit the budget first and remove everything else
before installing anything. The budget counts every existing copy as
free, but copies of modules that no longer fit were only removed at the
end, so a new module was refused for space while an older one that did
not fit kept its copy, and which module ran depended on install order.
The no_space message now shows the space actually free.

Write the status file and clear the boot counter even when the sync
fails part way: a read-only /metadata left the status of the last
successful sync in place, and an error after new content was installed
kept a parked counter, so the updated module never ran.

ksud insmod: fall back to /proc/kallsyms when KSU_IOCTL_LOOKUP_SYMBOLS
fails, not only on kernels before UAPI v6. The kallsyms walk moves into
ksuinit::resolve_kallsyms, which load_module now uses too.
@Ylarod
Ylarod merged commit 0faec0c into main Oct 16, 2026
24 checks passed
@Ylarod
Ylarod deleted the preinit branch October 16, 2026 13:31
chisewaguri pushed a commit to chisewaguri/KernelSU that referenced this pull request Oct 17, 2026
Adds a **preinit** mechanism that lets a module run a few actions
**before the SELinux policy is loaded and before `init.rc` is parsed** —
e.g. load a kernel module, run a static program, or write to
procfs/sysfs. A module describes them in a `preinit/` directory:

```
mymodule/
└── preinit/
    ├── preinit.conf      # one action per line: insmod / exec / write
    └── ...               # .ko files, programs, ...
```

- **Kernel runner**: a static, nolibc-built runner is embedded in the
kernel module (`.incbin`) and staged in a shmem file at boot. When PID 1
execs `/system/bin/init selinux_setup`, the execve hook swaps the runner
in via `/proc/self/fd/N`. The runner parses each module's
`preinit.conf`, performs the actions, then execs the real init. A boot
counter disables preinit after 3 failed boots in a row.
- **ksud** syncs each enabled module's `preinit/` dir to the preinit
partition (manifest-based, incremental, within a space budget on the
small `/metadata`). Storage dir is `/metadata/vold/ksu` when available
(hidden, does not change `/metadata`'s link count), else
`/metadata/watchdog/ksu`, else `/metadata/ksu`. `ksud module preinit
status|reset` inspects per-module state and clears the boot counter.
- `boot-patch` / `boot-patch-v2` gain `--no-preinit` (sets the
`preinit=0` kernel parameter).
- **Manager** shows a warning tag on modules whose preinit content was
not applied (no space / error / bootloop).
- **Docs**: `preinit.conf` format, actions, space budget, bootloop
recovery (EN + zh_CN).

- Verified on a Redmi K80 (android14-6.1): runner runs before policy
load (process context `kernel`), `/metadata` write + fsync survives
reboot, bootloop counter parks and `reset` recovers, all conf actions
(insmod/exec/write, optional `?`, quoting, exit codes, output
forwarding). Confirmed with both v1 (ksuinit in init_boot) and v2 (ko
injected into boot). `--no-preinit` disables it (`preinit: disabled`).
Charger mode (`androidboot.mode=charger`) is skipped and does not bump
the counter. Data lands under `/metadata/vold/ksu` with the
`vold_metadata_file` label; `/metadata` root link count is unchanged.
- Builds pass for all 8 KMIs (android12-5.10 … android17-6.18).

(cherry picked from commit 0faec0c)
pershoot pushed a commit to KernelSU-Next/KernelSU-Next that referenced this pull request Oct 17, 2026
…rnelSU#3907)

Adds a **preinit** mechanism that lets a module run a few actions
**before the SELinux policy is loaded and before `init.rc` is parsed** —
e.g. load a kernel module, run a static program, or write to
procfs/sysfs. A module describes them in a `preinit/` directory:

```
mymodule/
└── preinit/
    ├── preinit.conf      # one action per line: insmod / exec / write
    └── ...               # .ko files, programs, ...
```

- **Kernel runner**: a static, nolibc-built runner is embedded in the
kernel module (`.incbin`) and staged in a shmem file at boot. When PID 1
execs `/system/bin/init selinux_setup`, the execve hook swaps the runner
in via `/proc/self/fd/N`. The runner parses each module's
`preinit.conf`, performs the actions, then execs the real init. A boot
counter disables preinit after 3 failed boots in a row.
- **ksud** syncs each enabled module's `preinit/` dir to the preinit
partition (manifest-based, incremental, within a space budget on the
small `/metadata`). Storage dir is `/metadata/vold/ksu` when available
(hidden, does not change `/metadata`'s link count), else
`/metadata/watchdog/ksu`, else `/metadata/ksu`. `ksud module preinit
status|reset` inspects per-module state and clears the boot counter.
- `boot-patch` / `boot-patch-v2` gain `--no-preinit` (sets the
`preinit=0` kernel parameter).
- **Manager** shows a warning tag on modules whose preinit content was
not applied (no space / error / bootloop).
- **Docs**: `preinit.conf` format, actions, space budget, bootloop
recovery (EN + zh_CN).

- Verified on a Redmi K80 (android14-6.1): runner runs before policy
load (process context `kernel`), `/metadata` write + fsync survives
reboot, bootloop counter parks and `reset` recovers, all conf actions
(insmod/exec/write, optional `?`, quoting, exit codes, output
forwarding). Confirmed with both v1 (ksuinit in init_boot) and v2 (ko
injected into boot). `--no-preinit` disables it (`preinit: disabled`).
Charger mode (`androidboot.mode=charger`) is skipped and does not bump
the counter. Data lands under `/metadata/vold/ksu` with the
`vold_metadata_file` label; `/metadata` root link count is unchanged.
- Builds pass for all 8 KMIs (android12-5.10 … android17-6.18).

-com.rifsxd.ksunext: Port (Module.kt / ModuleViewModel.kt / ModulePreinit.kt)
-Omit: docs: module.md (doesn't exist; will be followed up)
omrxm18 added a commit to omrxm18/KernelSU-Next that referenced this pull request Oct 17, 2026
…rnelSU#3907)

Adds a **preinit** mechanism that lets a module run a few actions **before the SELinux policy is loaded and before  is parsed**

— e.g. load a kernel module, run a static program, or write to procfs/sysfs. A module describes them in a directory:

- **Kernel runner**: a static, nolibc-built runner is embedded in the kernel module () and staged in a shmem file at boot. When PID 1 execs , the execve hook swaps the runner in via . The runner parses each module's , performs the actions, then execs the real init. A boot counter disables preinit after 3 failed boots in a row.

- Verified on a Redmi Note 10s (android11-4.19): runner runs before policy load (process context),  write + fsync survives reboot, bootloop counter parks and  recovers, all conf actions (insmod/exec/write, optional , quoting, exit codes, output forwarding). Confirmed with both v1 (ksuinit in init_boot) and v2 (ko injected into boot).  disables it (). Charger mode () is skipped and does not bump the counter. Data lands under  with the  label;  root link count is unchanged.
omrxm18 added a commit to omrxm18/KernelSU-Next that referenced this pull request Oct 17, 2026
…rnelSU#3907)

Adds a **preinit** mechanism that lets a module run a few actions **before the SELinux policy is loaded and before  is parsed**

— e.g. load a kernel module, run a static program, or write to procfs/sysfs. A module describes them in a directory:

- **Kernel runner**: a static, nolibc-built runner is embedded in the kernel module () and staged in a shmem file at boot.
When PID 1 execs, the execve hook swaps the runner in via `/proc/self/fd/N`.
The runner parses each module's `preinit.conf, performs the actions, then execs the real init.
 A boot counter disables preinit after 3 failed boots in a row.

- Verified on a Redmi Note 10s (android11-4.19): runner runs before policy load (process context `kernel`), `/metadata` write + fsync survives reboot, bootloop counter parks and  recovers, all conf actions (insmod/exec/write, optional , quoting, exit codes, output forwarding). Confirmed with both v1 (ksuinit in init_boot) and v2 (ko injected into boot).  disables it (). Charger mode () is skipped and does not bump the counter. Data lands under  with the  label;  root link count is unchanged.
omrxm18 added a commit to omrxm18/KernelSU-Next that referenced this pull request Oct 17, 2026
…rnelSU#3907)

Adds a **preinit** mechanism that lets a module run a few actions **before the SELinux policy is loaded and before  is parsed**

— e.g. load a kernel module, run a static program, or write to procfs/sysfs. A module describes them in a directory:

- **Kernel runner**: a static, nolibc-built runner is embedded in the kernel module () and staged in a shmem file at boot.
When PID 1 execs, the execve hook swaps the runner in via `/proc/self/fd/N`.
The runner parses each module's `preinit.conf, performs the actions, then execs the real init.
 A boot counter disables preinit after 3 failed boots in a row.

- Verified on a Redmi Note 10s (android11-4.19): runner runs before policy load (process context `kernel`), `/metadata` write + fsync survives reboot, bootloop counter parks and  recovers, all conf actions (insmod/exec/write.
 optional quoting, exit codes, output forwarding).
Confirmed with both v1 (ksuinit in init_boot) and v2 (ko injected into boot).
`--no-preinit` disables it (). Charger mode () is skipped and does not bump the counter. Data lands under  with the  label;  root link count is unchanged.
maxsteeel pushed a commit to KernelSU-Next/KernelSU-Next that referenced this pull request Oct 17, 2026
…rnelSU#3907)

Adds a **preinit** mechanism that lets a module run a few actions **before the SELinux policy is loaded and before  is parsed**

— e.g. load a kernel module, run a static program, or write to procfs/sysfs. A module describes them in a directory:

- **Kernel runner**: a static, nolibc-built runner is embedded in the kernel module () and staged in a shmem file at boot.
When PID 1 execs, the execve hook swaps the runner in via `/proc/self/fd/N`.
The runner parses each module's `preinit.conf, performs the actions, then execs the real init.
 A boot counter disables preinit after 3 failed boots in a row.

- Verified on a Redmi Note 10s (android11-4.19): runner runs before policy load (process context `kernel`), `/metadata` write + fsync survives reboot, bootloop counter parks and  recovers, all conf actions (insmod/exec/write.
 optional quoting, exit codes, output forwarding).
Confirmed with both v1 (ksuinit in init_boot) and v2 (ko injected into boot).
`--no-preinit` disables it (). Charger mode () is skipped and does not bump the counter. Data lands under  with the  label;  root link count is unchanged.
GuitarHeroStyles added a commit to GuitarHeroStyles/kernel_samsung_sm7325 that referenced this pull request Oct 17, 2026
Pulls in 0b17f242 (tiann/KernelSU#3907): adds a preinit mechanism for
modules to run actions before SELinux policy loads. Falls back to
disabled with a build warning if UAPI headers for preinit are missing.
github-actions Bot added a commit to L7NEG/kernel_samsung_universal9611 that referenced this pull request Oct 18, 2026
…rnelSU#3907)

Recent legacy changes:

0b17f242 Add module preinit: run actions before SELinux policy loads (tiann/KernelSU#3907)
465490a2 kernel: selinux: never sleep while holding the policy rwlock write side
e3292239 legacy: port shell_su_adb_only and force_feature from dev 3.4.2
a56b9199 pkg_observer, sulog, manager: stop the deadlock, log su, unstick the queue
0550f9eb selinux: fix four policy-mutation defects
Anatdx pushed a commit to Rouyashiki/YukiSU that referenced this pull request Oct 19, 2026
Port preinit into the C++ module lifecycle and current Manager. Stage a
bounded runner at boot, sync enabled modules within the metadata reserve,
and retain bootloop recovery, status reporting and the disable option.
Add batched kernel symbol lookup while preserving existing YukiSU ioctls.

Port tiann/KernelSU#3907:
tiann/KernelSU#3907
Upstream commit: 0faec0cc7b5f655170d89bf9c4d28bb5ca271fa2

Co-authored-by: Ylarod <me@ylarod.cn>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant