Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions build-ksuinit.py
Original file line number Diff line number Diff line change
@@ -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)
23 changes: 23 additions & 0 deletions userspace/ksud/src/boot_patch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<()> {
Expand All @@ -550,6 +555,7 @@ pub fn patch(args: BootPatchArgs) -> Result<()> {
flash,
#[cfg(target_os = "android")]
partition,
preload_dumper_modules,
} = args;

println!(include_str!("banner"));
Expand Down Expand Up @@ -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)
Expand Down
16 changes: 16 additions & 0 deletions userspace/ksud/src/cli.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use anyhow::{Context, Ok, Result};
use clap::Parser;
use ksuinit::module_loader;
use std::path::PathBuf;

use android_logger::Config;
Expand Down Expand Up @@ -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<String>,
},
}

#[derive(clap::Subcommand, Debug)]
Expand Down Expand Up @@ -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),
Expand Down
17 changes: 17 additions & 0 deletions userspace/ksuinit/src/init.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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 {
Expand Down
2 changes: 2 additions & 0 deletions userspace/ksuinit/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ use scroll::{Pwrite, ctx::SizeWith};
use std::collections::HashMap;
use std::fs;

pub mod module_loader;

struct Kptr {
value: String,
}
Expand Down
140 changes: 140 additions & 0 deletions userspace/ksuinit/src/module_loader.rs
Original file line number Diff line number Diff line change
@@ -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<str>],
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<HashMap<PathBuf, Vec<PathBuf>>> {
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::<Vec<_>>();

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<PathBuf, Vec<PathBuf>>,
visiting: &mut HashSet<PathBuf>,
visited: &mut HashSet<PathBuf>,
ordered: &mut Vec<PathBuf>,
) -> 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(())
}
Loading