From 0e3c2ef3889845e56d30917aff8810c744160abb Mon Sep 17 00:00:00 2001 From: 5ec1cff Date: Mon, 23 Mar 2026 21:47:56 +0800 Subject: [PATCH 1/6] module loader --- userspace/ksuinit/src/lib.rs | 2 + userspace/ksuinit/src/module_loader.rs | 118 +++++++++++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 userspace/ksuinit/src/module_loader.rs diff --git a/userspace/ksuinit/src/lib.rs b/userspace/ksuinit/src/lib.rs index 283ca0481380..f284fb47793f 100644 --- a/userspace/ksuinit/src/lib.rs +++ b/userspace/ksuinit/src/lib.rs @@ -5,6 +5,8 @@ use scroll::{Pwrite, ctx::SizeWith}; use std::collections::HashMap; use std::fs; +pub mod module_loader; + struct Kptr { value: String, } diff --git a/userspace/ksuinit/src/module_loader.rs b/userspace/ksuinit/src/module_loader.rs new file mode 100644 index 000000000000..d2da90b8b54c --- /dev/null +++ b/userspace/ksuinit/src/module_loader.rs @@ -0,0 +1,118 @@ +use anyhow::{Context, Result, bail}; +use rustix::{ + cstr, + fd::AsFd, + system::finit_module, +}; +use std::{ + collections::{HashMap, HashSet}, + fs, + path::{Path, PathBuf}, +}; + +/// Load the requested kernel modules in dependency order using finit_module. +/// +/// `module_dir` should contain `.ko` files and a `modules.dep` file. +/// `requested_modules` entries follow the same path rules as `modules.dep`: +/// absolute paths are used as-is, and relative paths are resolved against `module_dir`. +pub fn load_modules_in_dependency_order( + module_dir: &Path, + requested_modules: &[impl AsRef], +) -> Result<()> { + let dep_graph = parse_modules_dep(module_dir)?; + let mut ordered = Vec::new(); + let mut visiting = HashSet::new(); + let mut visited = HashSet::new(); + + for module in requested_modules { + let module_path = resolve_module_path(module_dir, module.as_ref()); + dfs_visit( + &module_path, + &dep_graph, + &mut visiting, + &mut visited, + &mut ordered, + )?; + } + + for module in ordered { + load_single_module(&module)?; + } + + Ok(()) +} + +fn parse_modules_dep(module_dir: &Path) -> Result>> { + let dep_file = module_dir.join("modules.dep"); + let content = fs::read_to_string(&dep_file) + .with_context(|| format!("Failed to read {}", dep_file.display()))?; + + let mut graph = HashMap::new(); + for (line_no, line) in content.lines().enumerate() { + let line = line.trim(); + if line.is_empty() { + continue; + } + + let Some((module, deps)) = line.split_once(':') else { + bail!("Invalid modules.dep format at line {}: {}", line_no + 1, line); + }; + + let module_path = resolve_module_path(module_dir, module.trim()); + let dependencies = deps + .split_whitespace() + .map(|dep| resolve_module_path(module_dir, dep)) + .collect::>(); + + graph.insert(module_path, dependencies); + } + + Ok(graph) +} + +fn resolve_module_path(module_dir: &Path, module: &str) -> PathBuf { + let path = Path::new(module); + if path.is_absolute() { + path.to_path_buf() + } else { + module_dir.join(path) + } +} + +fn dfs_visit( + module: &Path, + dep_graph: &HashMap>, + visiting: &mut HashSet, + visited: &mut HashSet, + ordered: &mut Vec, +) -> Result<()> { + if visited.contains(module) { + return Ok(()); + } + + if !visiting.insert(module.to_path_buf()) { + bail!("Cyclic module dependency detected at {}", module.display()); + } + + if let Some(deps) = dep_graph.get(module) { + for dep in deps { + dfs_visit(dep, dep_graph, visiting, visited, ordered)?; + } + } + + visiting.remove(module); + visited.insert(module.to_path_buf()); + ordered.push(module.to_path_buf()); + Ok(()) +} + +fn load_single_module(module_path: &Path) -> Result<()> { + let file = fs::File::open(module_path) + .with_context(|| format!("Failed to open module {}", module_path.display()))?; + + // We intentionally pass empty parameters and default init flags for regular module loading. + finit_module(file.as_fd(), cstr!(""), 0) + .with_context(|| format!("finit_module failed for {}", module_path.display()))?; + + Ok(()) +} From 557b0d6c7ba604dc67241a6e31667ab31d0cd1df Mon Sep 17 00:00:00 2001 From: 5ec1cff Date: Sun, 5 Apr 2026 21:38:48 +0800 Subject: [PATCH 2/6] preload_minidump --- userspace/ksud/src/cli.rs | 16 ++++++++ userspace/ksuinit/src/init.rs | 17 +++++++++ userspace/ksuinit/src/module_loader.rs | 52 ++++++++++++++++++-------- 3 files changed, 69 insertions(+), 16 deletions(-) diff --git a/userspace/ksud/src/cli.rs b/userspace/ksud/src/cli.rs index 061d2b16ee38..17df90062478 100644 --- a/userspace/ksud/src/cli.rs +++ b/userspace/ksud/src/cli.rs @@ -1,5 +1,6 @@ use anyhow::{Context, Ok, Result}; use clap::Parser; +use ksuinit::module_loader; use std::path::PathBuf; use android_logger::Config; @@ -207,6 +208,15 @@ enum Debug { /// Launch sulogd daemon manually Sulogd, + + /// Test load modules + LoadMod { + #[arg(long)] + dir: PathBuf, + #[arg(long)] + root: PathBuf, + list: Vec, + }, } #[derive(clap::Subcommand, Debug)] @@ -691,6 +701,12 @@ pub fn run() -> Result<()> { MarkCommand::Refresh => debug::mark_refresh(), }, Debug::Sulogd => sulog::ensure_sulogd_running(), + Debug::LoadMod { dir, root, list } => module_loader::load_modules_in_dependency_order( + dir.as_path(), + root.as_path(), + list.as_slice(), + true, + ), }, Commands::BootPatch(boot_patch) => crate::boot_patch::patch(boot_patch), diff --git a/userspace/ksuinit/src/init.rs b/userspace/ksuinit/src/init.rs index 5a8a71f06cc9..da6c349a9557 100644 --- a/userspace/ksuinit/src/init.rs +++ b/userspace/ksuinit/src/init.rs @@ -1,6 +1,9 @@ +use std::fs; use std::io::{ErrorKind, Write}; +use std::path::Path; use anyhow::{Context, Result}; +use ksuinit::module_loader; use rustix::fs::{Mode, symlink, unlink}; use rustix::{ fd::AsFd, @@ -108,6 +111,20 @@ pub fn init() -> Result<()> { // This relies on the fact that we have /proc mounted unlimit_kmsg(); + // Currently, only qualcomm's minidump is supported. + // See https://xtuly.cn/article/oneplus-ace-5-panic-log + if let Ok(true) = fs::exists("/preload_minidump") { + log::info!("preload minidump requested!"); + if let Err(e) = module_loader::load_modules_in_dependency_order( + Path::new("/lib/modules"), + Path::new("/"), + &["minidump.ko"], + false, + ) { + log::error!("could not preload minidump modules: {e:?}"); + } + } + if ksuinit::has_kernelsu() { log::info!("KernelSU may be already loaded in kernel, skip!"); } else { diff --git a/userspace/ksuinit/src/module_loader.rs b/userspace/ksuinit/src/module_loader.rs index d2da90b8b54c..54246902cfb7 100644 --- a/userspace/ksuinit/src/module_loader.rs +++ b/userspace/ksuinit/src/module_loader.rs @@ -1,9 +1,5 @@ use anyhow::{Context, Result, bail}; -use rustix::{ - cstr, - fd::AsFd, - system::finit_module, -}; +use rustix::{cstr, fd::AsFd, system::finit_module}; use std::{ collections::{HashMap, HashSet}, fs, @@ -17,15 +13,24 @@ use std::{ /// absolute paths are used as-is, and relative paths are resolved against `module_dir`. pub fn load_modules_in_dependency_order( module_dir: &Path, + root_dir: &Path, requested_modules: &[impl AsRef], + dry_run: bool, ) -> Result<()> { - let dep_graph = parse_modules_dep(module_dir)?; + let dep_graph = parse_modules_dep(module_dir, root_dir)?; let mut ordered = Vec::new(); let mut visiting = HashSet::new(); let mut visited = HashSet::new(); for module in requested_modules { - let module_path = resolve_module_path(module_dir, module.as_ref()); + let module_path = resolve_module_path(module_dir, root_dir, module.as_ref()); + if !module_path.exists() { + log::info!( + "requested module {} not exists, skip preload!", + module_path.display() + ); + continue; + } dfs_visit( &module_path, &dep_graph, @@ -36,14 +41,18 @@ pub fn load_modules_in_dependency_order( } for module in ordered { - load_single_module(&module)?; + if dry_run { + println!("loading {}", module.display()); + } else { + load_single_module(&module)?; + } } Ok(()) } -fn parse_modules_dep(module_dir: &Path) -> Result>> { - let dep_file = module_dir.join("modules.dep"); +fn parse_modules_dep(module_dir: &Path, root_dir: &Path) -> Result>> { + let dep_file = resolve_module_path(module_dir, root_dir, "modules.dep"); let content = fs::read_to_string(&dep_file) .with_context(|| format!("Failed to read {}", dep_file.display()))?; @@ -55,13 +64,17 @@ fn parse_modules_dep(module_dir: &Path) -> Result> } let Some((module, deps)) = line.split_once(':') else { - bail!("Invalid modules.dep format at line {}: {}", line_no + 1, line); + bail!( + "Invalid modules.dep format at line {}: {}", + line_no + 1, + line + ); }; - let module_path = resolve_module_path(module_dir, module.trim()); + let module_path = resolve_module_path(module_dir, root_dir, module.trim()); let dependencies = deps .split_whitespace() - .map(|dep| resolve_module_path(module_dir, dep)) + .map(|dep| resolve_module_path(module_dir, root_dir, dep)) .collect::>(); graph.insert(module_path, dependencies); @@ -70,13 +83,18 @@ fn parse_modules_dep(module_dir: &Path) -> Result> Ok(graph) } -fn resolve_module_path(module_dir: &Path, module: &str) -> PathBuf { +fn resolve_module_path(module_dir: &Path, root_dir: &Path, module: &str) -> PathBuf { let path = Path::new(module); - if path.is_absolute() { + let orig_absolute_path = if path.is_absolute() { path.to_path_buf() } else { module_dir.join(path) - } + }; + root_dir.join( + orig_absolute_path + .strip_prefix("/") + .unwrap_or(orig_absolute_path.as_path()), + ) } fn dfs_visit( @@ -98,6 +116,8 @@ fn dfs_visit( for dep in deps { dfs_visit(dep, dep_graph, visiting, visited, ordered)?; } + } else { + bail!("no dependency found: {}", module.display()); } visiting.remove(module); From 2528415bcc36fa96b9e0f1b9e06019cfec2b0068 Mon Sep 17 00:00:00 2001 From: 5ec1cff Date: Sun, 5 Apr 2026 21:55:54 +0800 Subject: [PATCH 3/6] boot-patch: add --preload-dumper-modules --- userspace/ksud/src/boot_patch.rs | 23 +++++++++++++++++++++++ userspace/ksuinit/src/init.rs | 2 +- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/userspace/ksud/src/boot_patch.rs b/userspace/ksud/src/boot_patch.rs index 06f364ef8764..c8c20cf44b01 100644 --- a/userspace/ksud/src/boot_patch.rs +++ b/userspace/ksud/src/boot_patch.rs @@ -526,6 +526,11 @@ pub struct BootPatchArgs { /// Do not (re-)install kernelsu, only modify configs (allow_shell, etc.) #[arg(long, default_value = "false")] no_install: bool, + + /// Preload kernel modules for crash dump (e.g. Qualcomm's minidump.ko) before loading KernelSU LKM. + /// This may help to enable kernel panic dump during early boot stage. + #[arg(long, default_value = "false")] + preload_dumper_modules: bool, } pub fn patch(args: BootPatchArgs) -> Result<()> { @@ -550,6 +555,7 @@ pub fn patch(args: BootPatchArgs) -> Result<()> { flash, #[cfg(target_os = "android")] partition, + preload_dumper_modules, } = args; println!(include_str!("banner")); @@ -773,6 +779,23 @@ pub fn patch(args: BootPatchArgs) -> Result<()> { } } + if preload_dumper_modules { + println!("- Enabling preload dumper modules"); + { + let allow_shell_file = workdir.join("preload_dumpers"); + File::create(allow_shell_file)?; + } + do_cpio_cmd( + &magiskboot, + workdir, + ramdisk, + "add 0644 preload_dumpers preload_dumpers", + )?; + } else if do_cpio_cmd(&magiskboot, workdir, ramdisk, "exists preload_dumpers").is_ok() { + println!("- Disabling preload dumper modules"); + do_cpio_cmd(&magiskboot, workdir, ramdisk, "rm preload_dumpers").ok(); + } + println!("- Repacking boot image"); // magiskboot repack boot.img let status = Command::new(&magiskboot) diff --git a/userspace/ksuinit/src/init.rs b/userspace/ksuinit/src/init.rs index da6c349a9557..ac6e7a5378e4 100644 --- a/userspace/ksuinit/src/init.rs +++ b/userspace/ksuinit/src/init.rs @@ -113,7 +113,7 @@ pub fn init() -> Result<()> { // Currently, only qualcomm's minidump is supported. // See https://xtuly.cn/article/oneplus-ace-5-panic-log - if let Ok(true) = fs::exists("/preload_minidump") { + if let Ok(true) = fs::exists("/preload_dumpers") { log::info!("preload minidump requested!"); if let Err(e) = module_loader::load_modules_in_dependency_order( Path::new("/lib/modules"), From 268998297ac78af8c579c5e1106b430c568fa392 Mon Sep 17 00:00:00 2001 From: 5ec1cff Date: Sun, 5 Apr 2026 21:58:13 +0800 Subject: [PATCH 4/6] log --- userspace/ksuinit/src/module_loader.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/userspace/ksuinit/src/module_loader.rs b/userspace/ksuinit/src/module_loader.rs index 54246902cfb7..b65592c004d6 100644 --- a/userspace/ksuinit/src/module_loader.rs +++ b/userspace/ksuinit/src/module_loader.rs @@ -44,7 +44,9 @@ pub fn load_modules_in_dependency_order( if dry_run { println!("loading {}", module.display()); } else { - load_single_module(&module)?; + log::info!("preloading {}", module.display()); + load_single_module(&module) + .with_context(|| format!("preload {} failed", module.display()))?; } } From 804c37cc2d638854aa3e9b6212f51600f1ada246 Mon Sep 17 00:00:00 2001 From: 5ec1cff Date: Sun, 5 Apr 2026 23:25:31 +0800 Subject: [PATCH 5/6] build script --- build-ksuinit.py | 64 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 build-ksuinit.py diff --git a/build-ksuinit.py b/build-ksuinit.py new file mode 100644 index 000000000000..eaf9ae22b4e4 --- /dev/null +++ b/build-ksuinit.py @@ -0,0 +1,64 @@ + +import os +from pathlib import Path +import re +from subprocess import Popen +import sys +import shutil + +def version_key(version: str): + nums = re.findall(r"\d+", version) + return tuple(int(n) for n in nums) if nums else (0,) + +def find_ndk_bin(): + ndk_root = os.environ.get("ANDROID_NDK_HOME") or os.environ.get("ANDROID_NDK") + if not ndk_root: + sdk_root = os.environ.get("ANDROID_SDK_ROOT") or os.environ.get("ANDROID_HOME") + if sdk_root: + ndk_dir = Path(sdk_root) / "ndk" + if ndk_dir.exists(): + versions = sorted( + [d for d in ndk_dir.iterdir() if d.is_dir()], + key=lambda d: version_key(d.name), + reverse=True, + ) + if versions: + ndk_root = str(versions[0]) + + if ndk_root: + toolchain_bin = Path(ndk_root) / "toolchains" / "llvm" / "prebuilt" + if toolchain_bin.exists(): + return next(toolchain_bin.iterdir()) / "bin" + + raise FileNotFoundError('no android ndk bin found!') + + +def build_ksuinit(release): + bin = find_ndk_bin() + my_env = os.environ.copy() + CLANG = 'aarch64-linux-android26-clang' + STRIP = 'llvm-strip' + if os.name == 'nt': + CLANG += '.cmd' + STRIP += '.exe' + my_env['CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER'] = str(bin / CLANG) + my_env['RUSTFLAGS'] = '-C link-arg=-no-pie' + args = ['cargo', 'build', '-p', 'ksuinit', '--target', 'aarch64-unknown-linux-musl'] + if release: + args += ['--release'] + Popen( + args, + env=my_env + ).wait() + out_dir = Path('target/aarch64-unknown-linux-musl') / ('release' if release else 'debug') / 'ksuinit' + ksud_bin = Path('userspace/ksud/bin/aarch64/ksuinit') + shutil.copy(out_dir, ksud_bin) + print('copy ksud', out_dir, '->', ksud_bin) + Popen([str(bin / STRIP), str(ksud_bin)]).wait() + print('stripped', ksud_bin) + +if __name__ == '__main__': + release = False + if len(sys.argv) > 1: + release = sys.argv[1] == '--release' + build_ksuinit(release) From 720b5c2479c3759e744922facc466b57b0d82e85 Mon Sep 17 00:00:00 2001 From: 5ec1cff Date: Sun, 5 Apr 2026 23:25:38 +0800 Subject: [PATCH 6/6] load more --- userspace/ksuinit/src/init.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/userspace/ksuinit/src/init.rs b/userspace/ksuinit/src/init.rs index ac6e7a5378e4..dd0b8c8823de 100644 --- a/userspace/ksuinit/src/init.rs +++ b/userspace/ksuinit/src/init.rs @@ -118,7 +118,7 @@ pub fn init() -> Result<()> { if let Err(e) = module_loader::load_modules_in_dependency_order( Path::new("/lib/modules"), Path::new("/"), - &["minidump.ko"], + &["qcom_hwspinlock.ko", "minidump.ko", "qcom-scm.ko", "qcom_wdt_core.ko"], false, ) { log::error!("could not preload minidump modules: {e:?}");