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) 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/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..dd0b8c8823de 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_dumpers") { + log::info!("preload minidump requested!"); + if let Err(e) = module_loader::load_modules_in_dependency_order( + Path::new("/lib/modules"), + Path::new("/"), + &["qcom_hwspinlock.ko", "minidump.ko", "qcom-scm.ko", "qcom_wdt_core.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/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..b65592c004d6 --- /dev/null +++ b/userspace/ksuinit/src/module_loader.rs @@ -0,0 +1,140 @@ +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, + root_dir: &Path, + requested_modules: &[impl AsRef], + dry_run: bool, +) -> Result<()> { + 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, 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, + &mut visiting, + &mut visited, + &mut ordered, + )?; + } + + for module in ordered { + if dry_run { + println!("loading {}", module.display()); + } else { + log::info!("preloading {}", module.display()); + load_single_module(&module) + .with_context(|| format!("preload {} failed", module.display()))?; + } + } + + Ok(()) +} + +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()))?; + + 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, root_dir, module.trim()); + let dependencies = deps + .split_whitespace() + .map(|dep| resolve_module_path(module_dir, root_dir, dep)) + .collect::>(); + + graph.insert(module_path, dependencies); + } + + Ok(graph) +} + +fn resolve_module_path(module_dir: &Path, root_dir: &Path, module: &str) -> PathBuf { + let path = Path::new(module); + 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( + 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)?; + } + } else { + bail!("no dependency found: {}", module.display()); + } + + 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(()) +}