Updated main branch

This commit is contained in:
Fr4nz D13trich 2025-11-20 21:30:08 +01:00
parent 9a3987bc1e
commit 3f2388004d
350 changed files with 79360 additions and 0 deletions

2
userspace/ksud/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
/target
.cargo/

1835
userspace/ksud/Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

66
userspace/ksud/Cargo.toml Normal file
View file

@ -0,0 +1,66 @@
[package]
name = "ksud"
version = "0.1.0"
edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
notify = "8.2"
anyhow = "1"
clap = { version = "4", features = ["derive"] }
const_format = "0.2"
zip = { version = "6", features = [
"deflate",
"deflate64",
"time",
"lzma",
"xz",
], default-features = false }
zip-extensions = { version = "0.8", features = [
"deflate",
"lzma",
"xz",
], default-features = false }
java-properties = { git = "https://github.com/Kernel-SU/java-properties.git", branch = "master", default-features = false }
log = "0.4"
env_logger = { version = "0.11", default-features = false }
serde_json = "1"
encoding_rs = "0.8"
humansize = "2"
libc = "0.2"
extattr = "1"
jwalk = "0.8"
is_executable = "1"
nom = "8"
derive-new = "0.7"
rust-embed = { version = "8", features = [
"debug-embed",
"compression", # must clean build after updating binaries
] }
which = "8"
getopts = "0.2"
sha256 = "1"
sha1 = "0.10"
tempfile = "3"
chrono = "0.4"
regex-lite = "0.1"
serde = { version = "1.0", features = ["derive"] }
[target.'cfg(any(target_os = "android", target_os = "linux"))'.dependencies]
rustix = { git = "https://github.com/Kernel-SU/rustix.git", rev = "4a53fbc7cb7a07cabe87125cc21dbc27db316259", features = [
"all-apis",
] }
# some android specific dependencies which compiles under unix are also listed here for convenience of coding
android-properties = { version = "0.2", features = ["bionic-deprecated"] }
[target.'cfg(target_os = "android")'.dependencies]
android_logger = { version = "0.15", default-features = false }
[profile.release]
overflow-checks = false
codegen-units = 1
lto = "fat"
opt-level = 3
strip = true
split-debuginfo = "unpacked"

1
userspace/ksud/bin/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
**/*.ko

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
userspace/ksud/bin/arm/resetprop Executable file

Binary file not shown.

BIN
userspace/ksud/bin/x86_64/busybox Executable file

Binary file not shown.

BIN
userspace/ksud/bin/x86_64/ksuinit Executable file

Binary file not shown.

Binary file not shown.

47
userspace/ksud/build.rs Normal file
View file

@ -0,0 +1,47 @@
use std::{env, fs::File, io::Write, path::Path, process::Command};
fn get_git_version() -> Result<(u32, String), std::io::Error> {
let output = Command::new("git")
.args(["rev-list", "--count", "HEAD"])
.output()?;
let output = output.stdout;
let version_code = String::from_utf8(output).expect("Failed to read git count stdout");
let version_code: u32 = version_code
.trim()
.parse()
.map_err(|_| std::io::Error::other("Failed to parse git count"))?;
let version_code = 40000 - 2815 + version_code; // For historical reasons
let version_name = String::from_utf8(
Command::new("git")
.args(["describe", "--tags", "--always"])
.output()?
.stdout,
)
.map_err(|_| std::io::Error::other("Failed to parse git count"))?;
let version_name = version_name.trim_start_matches('v').to_string();
Ok((version_code, version_name))
}
fn main() {
let (code, name) = match get_git_version() {
Ok((code, name)) => (code, name),
Err(_) => {
// show warning if git is not installed
println!("cargo:warning=Failed to get git version, using 0.0.0");
(0, "0.0.0".to_string())
}
};
let out_dir = env::var("OUT_DIR").expect("Failed to get $OUT_DIR");
let out_dir = Path::new(&out_dir);
File::create(Path::new(out_dir).join("VERSION_CODE"))
.expect("Failed to create VERSION_CODE")
.write_all(code.to_string().as_bytes())
.expect("Failed to write VERSION_CODE");
File::create(Path::new(out_dir).join("VERSION_NAME"))
.expect("Failed to create VERSION_NAME")
.write_all(name.trim().as_bytes())
.expect("Failed to write VERSION_NAME");
}

View file

@ -0,0 +1,115 @@
use anyhow::{Result, ensure};
use std::io::{Read, Seek, SeekFrom};
pub fn get_apk_signature(apk: &str) -> Result<(u32, String)> {
let mut buffer = [0u8; 0x10];
let mut size4 = [0u8; 4];
let mut size8 = [0u8; 8];
let mut size_of_block = [0u8; 8];
let mut f = std::fs::File::open(apk)?;
let mut i = 0;
loop {
let mut n = [0u8; 2];
f.seek(SeekFrom::End(-i - 2))?;
f.read_exact(&mut n)?;
let n = u16::from_le_bytes(n);
if i64::from(n) == i {
f.seek(SeekFrom::Current(-22))?;
f.read_exact(&mut size4)?;
if u32::from_le_bytes(size4) ^ 0xcafe_babe_u32 == 0xccfb_f1ee_u32 {
if i > 0 {
println!("warning: comment length is {i}");
}
break;
}
}
ensure!(n != 0xffff, "not a zip file");
i += 1;
}
f.seek(SeekFrom::Current(12))?;
// offset
f.read_exact(&mut size4)?;
f.seek(SeekFrom::Start(u64::from(u32::from_le_bytes(size4)) - 0x18))?;
f.read_exact(&mut size8)?;
f.read_exact(&mut buffer)?;
ensure!(&buffer == b"APK Sig Block 42", "Can not found sig block");
let pos = u64::from(u32::from_le_bytes(size4)) - (u64::from_le_bytes(size8) + 0x8);
f.seek(SeekFrom::Start(pos))?;
f.read_exact(&mut size_of_block)?;
ensure!(size_of_block == size8, "not a signed apk");
let mut v2_signing: Option<(u32, String)> = None;
let mut v3_signing_exist = false;
let mut v3_1_signing_exist = false;
loop {
let mut id = [0u8; 4];
let mut offset = 4u32;
f.read_exact(&mut size8)?; // sequence length
if size8 == size_of_block {
break;
}
f.read_exact(&mut id)?; // id
let id = u32::from_le_bytes(id);
if id == 0x7109_871a_u32 {
v2_signing = Some(calc_cert_sha256(&mut f, &mut size4, &mut offset)?);
} else if id == 0xf053_68c0_u32 {
// v3 signature scheme
v3_signing_exist = true;
} else if id == 0x1b93_ad61_u32 {
// v3.1 signature scheme: credits to vvb2060
v3_1_signing_exist = true;
}
f.seek(SeekFrom::Current(
i64::from_le_bytes(size8) - i64::from(offset),
))?;
}
if v3_signing_exist || v3_1_signing_exist {
return Err(anyhow::anyhow!("Unexpected v3 signature found!",));
}
v2_signing.ok_or(anyhow::anyhow!("No signature found!"))
}
fn calc_cert_sha256(
f: &mut std::fs::File,
size4: &mut [u8; 4],
offset: &mut u32,
) -> Result<(u32, String)> {
f.read_exact(size4)?; // signer-sequence length
f.read_exact(size4)?; // signer length
f.read_exact(size4)?; // signed data length
*offset += 0x4 * 3;
f.read_exact(size4)?; // digests-sequence length
let pos = u32::from_le_bytes(*size4); // skip digests
f.seek(SeekFrom::Current(i64::from(pos)))?;
*offset += 0x4 + pos;
f.read_exact(size4)?; // certificates length
f.read_exact(size4)?; // certificate length
*offset += 0x4 * 2;
let cert_len = u32::from_le_bytes(*size4);
let mut cert: Vec<u8> = vec![0; cert_len as usize];
f.read_exact(&mut cert)?;
*offset += cert_len;
Ok((cert_len, sha256::digest(&cert)))
}

View file

@ -0,0 +1,55 @@
use anyhow::Result;
use const_format::concatcp;
use rust_embed::RustEmbed;
use std::path::Path;
use crate::{defs::BINARY_DIR, utils};
pub const RESETPROP_PATH: &str = concatcp!(BINARY_DIR, "resetprop");
pub const BUSYBOX_PATH: &str = concatcp!(BINARY_DIR, "busybox");
pub const BOOTCTL_PATH: &str = concatcp!(BINARY_DIR, "bootctl");
#[cfg(all(target_arch = "x86_64", target_os = "android"))]
#[derive(RustEmbed)]
#[folder = "bin/x86_64"]
struct Asset;
// IF NOT x86_64 ANDROID, ie. macos, linux, windows, always use aarch64
#[cfg(all(target_arch = "aarch64", target_os = "android"))]
#[derive(RustEmbed)]
#[folder = "bin/aarch64"]
struct Asset;
#[cfg(all(target_arch = "arm", target_os = "android"))]
#[derive(RustEmbed)]
#[folder = "bin/arm"]
struct Asset;
pub fn ensure_binaries(ignore_if_exist: bool) -> Result<()> {
for file in Asset::iter() {
if file == "ksuinit" || file.ends_with(".ko") {
// don't extract ksuinit and kernel modules
continue;
}
let asset = Asset::get(&file).ok_or(anyhow::anyhow!("asset not found: {}", file))?;
utils::ensure_binary(format!("{BINARY_DIR}{file}"), &asset.data, ignore_if_exist)?
}
Ok(())
}
pub fn copy_assets_to_file(name: &str, dst: impl AsRef<Path>) -> Result<()> {
let asset = Asset::get(name).ok_or(anyhow::anyhow!("asset not found: {}", name))?;
std::fs::write(dst, asset.data)?;
Ok(())
}
pub fn list_supported_kmi() -> Result<Vec<String>> {
let mut list = Vec::new();
for file in Asset::iter() {
// kmi_name = "xxx_kernelsu.ko"
if let Some(kmi) = file.strip_suffix("_kernelsu.ko") {
list.push(kmi.to_string());
}
}
Ok(list)
}

10
userspace/ksud/src/banner Normal file
View file

@ -0,0 +1,10 @@
____ _ _ ____ _ _
/ ___| _ _| | _(_) ___|| | | |
\___ \| | | | |/ / \___ \| | | |
___) | |_| | <| |___) | |_| |
|____/ \__,_|_|\_\_|____/ \___/
_ _ _ _
| | | | | |_ _ __ __ _
| | | | | __| '__/ _\ |
| |_| | | |_| | | (_| |
\___/|_|\__|_| \__,_|

View file

@ -0,0 +1,791 @@
#[cfg(unix)]
use std::{
os::unix::fs::PermissionsExt,
path::{Path, PathBuf},
process::{Command, Stdio},
};
use anyhow::{Context, Result, anyhow, bail, ensure};
use regex_lite::Regex;
use which::which;
use crate::{
assets,
defs::{self, BACKUP_FILENAME, KSU_BACKUP_DIR, KSU_BACKUP_FILE_PREFIX},
utils,
};
#[cfg(target_os = "android")]
fn ensure_gki_kernel() -> Result<()> {
let version = get_kernel_version()?;
let is_gki = version.0 == 5 && version.1 >= 10 || version.2 > 5;
ensure!(is_gki, "only support GKI kernel");
Ok(())
}
#[cfg(target_os = "android")]
fn get_kernel_version() -> Result<(i32, i32, i32)> {
let uname = rustix::system::uname();
let version = uname.release().to_string_lossy();
let re = Regex::new(r"(\d+)\.(\d+)\.(\d+)")?;
if let Some(captures) = re.captures(&version) {
let major = captures
.get(1)
.and_then(|m| m.as_str().parse::<i32>().ok())
.ok_or_else(|| anyhow!("Major version parse error"))?;
let minor = captures
.get(2)
.and_then(|m| m.as_str().parse::<i32>().ok())
.ok_or_else(|| anyhow!("Minor version parse error"))?;
let patch = captures
.get(3)
.and_then(|m| m.as_str().parse::<i32>().ok())
.ok_or_else(|| anyhow!("Patch version parse error"))?;
Ok((major, minor, patch))
} else {
Err(anyhow!("Invalid kernel version string"))
}
}
#[cfg(target_os = "android")]
fn parse_kmi(version: &str) -> Result<String> {
let re = Regex::new(r"(.* )?(\d+\.\d+)(\S+)?(android\d+)(.*)")?;
let cap = re
.captures(version)
.ok_or_else(|| anyhow::anyhow!("Failed to get KMI from boot/modules"))?;
let android_version = cap.get(4).map_or("", |m| m.as_str());
let kernel_version = cap.get(2).map_or("", |m| m.as_str());
Ok(format!("{android_version}-{kernel_version}"))
}
#[cfg(target_os = "android")]
fn parse_kmi_from_uname() -> Result<String> {
let uname = rustix::system::uname();
let version = uname.release().to_string_lossy();
parse_kmi(&version)
}
#[cfg(target_os = "android")]
fn parse_kmi_from_modules() -> Result<String> {
use std::io::BufRead;
// find a *.ko in /vendor/lib/modules
let modfile = std::fs::read_dir("/vendor/lib/modules")?
.filter_map(Result::ok)
.find(|entry| entry.path().extension().is_some_and(|ext| ext == "ko"))
.map(|entry| entry.path())
.ok_or_else(|| anyhow!("No kernel module found"))?;
let output = Command::new("modinfo").arg(modfile).output()?;
for line in output.stdout.lines().map_while(Result::ok) {
if line.starts_with("vermagic") {
return parse_kmi(&line);
}
}
anyhow::bail!("Parse KMI from modules failed")
}
#[cfg(target_os = "android")]
pub fn get_current_kmi() -> Result<String> {
parse_kmi_from_uname().or_else(|_| parse_kmi_from_modules())
}
#[cfg(not(target_os = "android"))]
pub fn get_current_kmi() -> Result<String> {
bail!("Unsupported platform")
}
fn parse_kmi_from_kernel(kernel: &PathBuf, workdir: &Path) -> Result<String> {
use std::fs::{File, copy};
use std::io::{BufReader, Read};
let kernel_path = workdir.join("kernel");
copy(kernel, &kernel_path).context("Failed to copy kernel")?;
let file = File::open(&kernel_path).context("Failed to open kernel file")?;
let mut reader = BufReader::new(file);
let mut buffer = Vec::new();
reader
.read_to_end(&mut buffer)
.context("Failed to read kernel file")?;
let printable_strings: Vec<&str> = buffer
.split(|&b| b == 0)
.filter_map(|slice| std::str::from_utf8(slice).ok())
.filter(|s| s.chars().all(|c| c.is_ascii_graphic() || c == ' '))
.collect();
let re =
Regex::new(r"(?:.* )?(\d+\.\d+)(?:\S+)?(android\d+)").context("Failed to compile regex")?;
for s in printable_strings {
if let Some(caps) = re.captures(s)
&& let (Some(kernel_version), Some(android_version)) = (caps.get(1), caps.get(2))
{
let kmi = format!("{}-{}", android_version.as_str(), kernel_version.as_str());
return Ok(kmi);
}
}
println!("- Failed to get KMI version");
bail!("Try to choose LKM manually")
}
fn parse_kmi_from_boot(magiskboot: &Path, image: &PathBuf, workdir: &Path) -> Result<String> {
let image_path = workdir.join("image");
std::fs::copy(image, &image_path).context("Failed to copy image")?;
let status = Command::new(magiskboot)
.current_dir(workdir)
.stdout(Stdio::null())
.stderr(Stdio::null())
.arg("unpack")
.arg(&image_path)
.status()
.context("Failed to execute magiskboot command")?;
if !status.success() {
bail!(
"magiskboot unpack failed with status: {:?}",
status.code().unwrap()
);
}
parse_kmi_from_kernel(&image_path, workdir)
}
fn do_cpio_cmd(magiskboot: &Path, workdir: &Path, cpio_path: &Path, cmd: &str) -> Result<()> {
let status = Command::new(magiskboot)
.current_dir(workdir)
.stdout(Stdio::null())
.stderr(Stdio::null())
.arg("cpio")
.arg(cpio_path)
.arg(cmd)
.status()?;
ensure!(status.success(), "magiskboot cpio {} failed", cmd);
Ok(())
}
fn is_magisk_patched(magiskboot: &Path, workdir: &Path, cpio_path: &Path) -> Result<bool> {
let status = Command::new(magiskboot)
.current_dir(workdir)
.stdout(Stdio::null())
.stderr(Stdio::null())
.arg("cpio")
.arg(cpio_path)
.arg("test")
.status()?;
// 0: stock, 1: magisk
Ok(status.code() == Some(1))
}
fn is_kernelsu_patched(magiskboot: &Path, workdir: &Path, cpio_path: &Path) -> Result<bool> {
let status = Command::new(magiskboot)
.current_dir(workdir)
.stdout(Stdio::null())
.stderr(Stdio::null())
.arg("cpio")
.arg(cpio_path)
.arg("exists kernelsu.ko")
.status()?;
Ok(status.success())
}
fn dd<P: AsRef<Path>, Q: AsRef<Path>>(ifile: P, ofile: Q) -> Result<()> {
let status = Command::new("dd")
.stdout(Stdio::null())
.stderr(Stdio::null())
.arg(format!("if={}", ifile.as_ref().display()))
.arg(format!("of={}", ofile.as_ref().display()))
.status()?;
ensure!(
status.success(),
"dd if={:?} of={:?} failed",
ifile.as_ref(),
ofile.as_ref()
);
Ok(())
}
pub fn restore(
image: Option<PathBuf>,
magiskboot_path: Option<PathBuf>,
flash: bool,
) -> Result<()> {
let tmpdir = tempfile::Builder::new()
.prefix("KernelSU")
.tempdir()
.context("create temp dir failed")?;
let workdir = tmpdir.path();
let magiskboot = find_magiskboot(magiskboot_path, workdir)?;
let kmi = get_current_kmi().unwrap_or_else(|_| String::from(""));
let (bootimage, bootdevice) = find_boot_image(&image, &kmi, false, false, workdir, &None)?;
println!("- Unpacking boot image");
let status = Command::new(&magiskboot)
.current_dir(workdir)
.stdout(Stdio::null())
.stderr(Stdio::null())
.arg("unpack")
.arg(bootimage.display().to_string())
.status()?;
ensure!(status.success(), "magiskboot unpack failed");
let mut ramdisk = workdir.join("ramdisk.cpio");
if !ramdisk.exists() {
ramdisk = workdir.join("vendor_ramdisk").join("init_boot.cpio")
}
if !ramdisk.exists() {
ramdisk = workdir.join("vendor_ramdisk").join("ramdisk.cpio");
}
if !ramdisk.exists() {
bail!("No compatible ramdisk found.")
}
let ramdisk = ramdisk.as_path();
let is_kernelsu_patched = is_kernelsu_patched(&magiskboot, workdir, ramdisk)?;
ensure!(is_kernelsu_patched, "boot image is not patched by KernelSU");
let mut new_boot = None;
let mut from_backup = false;
#[cfg(target_os = "android")]
if do_cpio_cmd(
&magiskboot,
workdir,
ramdisk,
&format!("exists {BACKUP_FILENAME}"),
)
.is_ok()
{
do_cpio_cmd(
&magiskboot,
workdir,
ramdisk,
&format!("extract {BACKUP_FILENAME} {BACKUP_FILENAME}"),
)?;
let sha = std::fs::read(workdir.join(BACKUP_FILENAME))?;
let sha = String::from_utf8(sha)?;
let sha = sha.trim();
let backup_path =
PathBuf::from(KSU_BACKUP_DIR).join(format!("{KSU_BACKUP_FILE_PREFIX}{sha}"));
if backup_path.is_file() {
new_boot = Some(backup_path);
from_backup = true;
} else {
println!("- Warning: no backup {backup_path:?} found!");
}
if let Err(e) = clean_backup(sha) {
println!("- Warning: Cleanup backup image failed: {e}");
}
} else {
println!("- Backup info is absent!");
}
if new_boot.is_none() {
// remove kernelsu.ko
do_cpio_cmd(&magiskboot, workdir, ramdisk, "rm kernelsu.ko")?;
// if init.real exists, restore it
let status = do_cpio_cmd(&magiskboot, workdir, ramdisk, "exists init.real").is_ok();
if status {
do_cpio_cmd(&magiskboot, workdir, ramdisk, "mv init.real init")?;
}
println!("- Repacking boot image");
let status = Command::new(&magiskboot)
.current_dir(workdir)
.stdout(Stdio::null())
.stderr(Stdio::null())
.arg("repack")
.arg(&bootimage)
.status()?;
ensure!(status.success(), "magiskboot repack failed");
new_boot = Some(workdir.join("new-boot.img"));
}
let new_boot = new_boot.unwrap();
if image.is_some() {
// if image is specified, write to output file
let output_dir = std::env::current_dir()?;
let now = chrono::Utc::now();
let output_image = output_dir.join(format!(
"kernelsu_restore_{}.img",
now.format("%Y%m%d_%H%M%S")
));
if from_backup || std::fs::rename(&new_boot, &output_image).is_err() {
std::fs::copy(&new_boot, &output_image).context("copy out new boot failed")?;
}
println!("- Output file is written to");
println!("- {}", output_image.display().to_string().trim_matches('"'));
}
if flash {
if from_backup {
println!("- Flashing new boot image from {}", new_boot.display());
} else {
println!("- Flashing new boot image");
}
flash_boot(&bootdevice, new_boot)?;
}
println!("- Done!");
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub fn patch(
image: Option<PathBuf>,
kernel: Option<PathBuf>,
kmod: Option<PathBuf>,
init: Option<PathBuf>,
ota: bool,
flash: bool,
out: Option<PathBuf>,
magiskboot: Option<PathBuf>,
kmi: Option<String>,
partition: Option<String>,
) -> Result<()> {
let result = do_patch(
image, kernel, kmod, init, ota, flash, out, magiskboot, kmi, partition,
);
if let Err(ref e) = result {
println!("- Install Error: {e}");
}
result
}
#[allow(clippy::too_many_arguments)]
fn do_patch(
image: Option<PathBuf>,
kernel: Option<PathBuf>,
kmod: Option<PathBuf>,
init: Option<PathBuf>,
ota: bool,
flash: bool,
out: Option<PathBuf>,
magiskboot_path: Option<PathBuf>,
kmi: Option<String>,
partition: Option<String>,
) -> Result<()> {
println!(include_str!("banner"));
let patch_file = image.is_some();
#[cfg(target_os = "android")]
if !patch_file {
ensure_gki_kernel()?;
}
let is_replace_kernel = kernel.is_some();
if is_replace_kernel {
ensure!(
init.is_none() && kmod.is_none(),
"init and module must not be specified."
);
}
let tmpdir = tempfile::Builder::new()
.prefix("KernelSU")
.tempdir()
.context("create temp dir failed")?;
let workdir = tmpdir.path();
// extract magiskboot
let magiskboot = find_magiskboot(magiskboot_path, workdir)?;
let kmi = if let Some(kmi) = kmi {
kmi
} else {
match get_current_kmi() {
Ok(value) => value,
Err(e) => {
println!("- {e}");
if let Some(image_path) = &image {
println!(
"- Trying to auto detect KMI version for {}",
image_path.to_str().unwrap()
);
parse_kmi_from_boot(&magiskboot, image_path, tmpdir.path())?
} else if let Some(kernel_path) = &kernel {
println!(
"- Trying to auto detect KMI version for {}",
kernel_path.to_str().unwrap()
);
parse_kmi_from_kernel(kernel_path, tmpdir.path())?
} else {
"".to_string()
}
}
}
};
let (bootimage, bootdevice) =
find_boot_image(&image, &kmi, ota, is_replace_kernel, workdir, &partition)?;
let bootimage = bootimage.as_path();
// try extract magiskboot/bootctl
let _ = assets::ensure_binaries(false);
if let Some(kernel) = kernel {
std::fs::copy(kernel, workdir.join("kernel")).context("copy kernel from failed")?;
}
println!("- Preparing assets");
let kmod_file = workdir.join("kernelsu.ko");
if let Some(kmod) = kmod {
std::fs::copy(kmod, kmod_file).context("copy kernel module failed")?;
} else {
// If kmod is not specified, extract from assets
println!("- KMI: {kmi}");
let name = format!("{kmi}_kernelsu.ko");
assets::copy_assets_to_file(&name, kmod_file)
.with_context(|| format!("Failed to copy {name}"))?;
};
let init_file = workdir.join("init");
if let Some(init) = init {
std::fs::copy(init, init_file).context("copy init failed")?;
} else {
assets::copy_assets_to_file("ksuinit", init_file).context("copy ksuinit failed")?;
}
println!("- Unpacking boot image");
let status = Command::new(&magiskboot)
.current_dir(workdir)
.stdout(Stdio::null())
.stderr(Stdio::null())
.arg("unpack")
.arg(bootimage)
.status()?;
ensure!(status.success(), "magiskboot unpack failed");
let mut ramdisk = workdir.join("ramdisk.cpio");
if !ramdisk.exists() {
ramdisk = workdir.join("vendor_ramdisk").join("init_boot.cpio")
}
if !ramdisk.exists() {
ramdisk = workdir.join("vendor_ramdisk").join("ramdisk.cpio");
}
if !ramdisk.exists() {
println!("- No ramdisk, create by default");
ramdisk = "ramdisk.cpio".into();
}
let ramdisk = ramdisk.as_path();
let is_magisk_patched = is_magisk_patched(&magiskboot, workdir, ramdisk)?;
ensure!(!is_magisk_patched, "Cannot work with Magisk patched image");
println!("- Adding KernelSU LKM");
let is_kernelsu_patched = is_kernelsu_patched(&magiskboot, workdir, ramdisk)?;
let mut need_backup = false;
if !is_kernelsu_patched {
// kernelsu.ko is not exist, backup init if necessary
let status = do_cpio_cmd(&magiskboot, workdir, ramdisk, "exists init");
if status.is_ok() {
do_cpio_cmd(&magiskboot, workdir, ramdisk, "mv init init.real")?;
}
need_backup = flash;
}
do_cpio_cmd(&magiskboot, workdir, ramdisk, "add 0755 init init")?;
do_cpio_cmd(
&magiskboot,
workdir,
ramdisk,
"add 0755 kernelsu.ko kernelsu.ko",
)?;
#[cfg(target_os = "android")]
if need_backup && let Err(e) = do_backup(&magiskboot, workdir, ramdisk, bootimage) {
println!("- Backup stock image failed: {e}");
}
println!("- Repacking boot image");
// magiskboot repack boot.img
let status = Command::new(&magiskboot)
.current_dir(workdir)
.stdout(Stdio::null())
.stderr(Stdio::null())
.arg("repack")
.arg(bootimage)
.status()?;
ensure!(status.success(), "magiskboot repack failed");
let new_boot = workdir.join("new-boot.img");
if patch_file {
// if image is specified, write to output file
let output_dir = out.unwrap_or(std::env::current_dir()?);
let now = chrono::Utc::now();
let output_image = output_dir.join(format!(
"kernelsu_patched_{}.img",
now.format("%Y%m%d_%H%M%S")
));
if std::fs::rename(&new_boot, &output_image).is_err() {
std::fs::copy(&new_boot, &output_image).context("copy out new boot failed")?;
}
println!("- Output file is written to");
println!("- {}", output_image.display().to_string().trim_matches('"'));
}
if flash {
println!("- Flashing new boot image");
flash_boot(&bootdevice, new_boot)?;
if ota {
post_ota()?;
}
}
println!("- Done!");
Ok(())
}
#[cfg(target_os = "android")]
fn calculate_sha1(file_path: impl AsRef<Path>) -> Result<String> {
use sha1::Digest;
use std::io::Read;
let mut file = std::fs::File::open(file_path.as_ref())?;
let mut hasher = sha1::Sha1::new();
let mut buffer = [0; 1024];
loop {
let n = file.read(&mut buffer)?;
if n == 0 {
break;
}
hasher.update(&buffer[..n]);
}
let result = hasher.finalize();
Ok(format!("{result:x}"))
}
#[cfg(target_os = "android")]
fn do_backup(magiskboot: &Path, workdir: &Path, cpio_path: &Path, image: &Path) -> Result<()> {
let sha1 = calculate_sha1(image)?;
let filename = format!("{KSU_BACKUP_FILE_PREFIX}{sha1}");
println!("- Backup stock boot image");
// magiskboot cpio ramdisk.cpio 'add 0755 $BACKUP_FILENAME'
let target = format!("{KSU_BACKUP_DIR}{filename}");
std::fs::copy(image, &target).with_context(|| format!("backup to {target}"))?;
std::fs::write(workdir.join(BACKUP_FILENAME), sha1.as_bytes()).context("write sha1")?;
do_cpio_cmd(
magiskboot,
workdir,
cpio_path,
&format!("add 0755 {BACKUP_FILENAME} {BACKUP_FILENAME}"),
)?;
println!("- Stock image has been backup to");
println!("- {target}");
Ok(())
}
#[cfg(target_os = "android")]
fn clean_backup(sha1: &str) -> Result<()> {
println!("- Clean up backup");
let backup_name = format!("{KSU_BACKUP_FILE_PREFIX}{sha1}");
let dir = std::fs::read_dir(defs::KSU_BACKUP_DIR)?;
for entry in dir.flatten() {
let path = entry.path();
if !path.is_file() {
continue;
}
if let Some(name) = path.file_name() {
let name = name.to_string_lossy().to_string();
if name != backup_name
&& name.starts_with(KSU_BACKUP_FILE_PREFIX)
&& std::fs::remove_file(path).is_ok()
{
println!("- removed {name}");
}
}
}
Ok(())
}
fn flash_boot(bootdevice: &Option<String>, new_boot: PathBuf) -> Result<()> {
let Some(bootdevice) = bootdevice else {
bail!("boot device not found")
};
let status = Command::new("blockdev")
.arg("--setrw")
.arg(bootdevice)
.status()?;
ensure!(status.success(), "set boot device rw failed");
dd(new_boot, bootdevice).context("flash boot failed")?;
Ok(())
}
fn find_magiskboot(magiskboot_path: Option<PathBuf>, workdir: &Path) -> Result<PathBuf> {
let magiskboot = {
if which("magiskboot").is_ok() {
let _ = assets::ensure_binaries(true);
"magiskboot".into()
} else {
// magiskboot is not in $PATH, use builtin or specified one
let magiskboot = if let Some(magiskboot_path) = magiskboot_path {
std::fs::canonicalize(magiskboot_path)?
} else {
let magiskboot_path = workdir.join("magiskboot");
assets::copy_assets_to_file("magiskboot", &magiskboot_path)
.context("copy magiskboot failed")?;
magiskboot_path
};
ensure!(magiskboot.exists(), "{magiskboot:?} is not exist");
#[cfg(unix)]
let _ = std::fs::set_permissions(&magiskboot, std::fs::Permissions::from_mode(0o755));
magiskboot
}
};
Ok(magiskboot)
}
fn find_boot_image(
image: &Option<PathBuf>,
kmi: &str,
ota: bool,
is_replace_kernel: bool,
workdir: &Path,
partition: &Option<String>,
) -> Result<(PathBuf, Option<String>)> {
let bootimage;
let mut bootdevice = None;
if let Some(ref image) = *image {
ensure!(image.exists(), "boot image not found");
bootimage = std::fs::canonicalize(image)?;
} else {
if cfg!(not(target_os = "android")) {
println!("- Current OS is not android, refusing auto bootimage/bootdevice detection");
bail!("Please specify a boot image");
}
let slot_suffix = get_slot_suffix(ota);
let boot_partition_name = choose_boot_partition(kmi, is_replace_kernel, partition);
let boot_partition = format!("/dev/block/by-name/{boot_partition_name}{slot_suffix}");
println!("- Bootdevice: {boot_partition}");
let tmp_boot_path = workdir.join("boot.img");
dd(&boot_partition, &tmp_boot_path)?;
ensure!(tmp_boot_path.exists(), "boot image not found");
bootimage = tmp_boot_path;
bootdevice = Some(boot_partition);
};
Ok((bootimage, bootdevice))
}
#[cfg(target_os = "android")]
pub fn choose_boot_partition(
kmi: &str,
is_replace_kernel: bool,
partition: &Option<String>,
) -> String {
let slot_suffix = get_slot_suffix(false);
let skip_init_boot = kmi.starts_with("android12-");
let init_boot_exist = Path::new(&format!("/dev/block/by-name/init_boot{slot_suffix}")).exists();
// if specific partition is specified, use it
if let Some(part) = partition {
return match part.as_str() {
"boot" | "init_boot" | "vendor_boot" => part.clone(),
_ => "boot".to_string(),
};
}
// if init_boot exists and not skipping it, use it
if !is_replace_kernel && init_boot_exist && !skip_init_boot {
return "init_boot".to_string();
}
"boot".to_string()
}
#[cfg(not(target_os = "android"))]
pub fn choose_boot_partition(
_kmi: &str,
_is_replace_kernel: bool,
_partition: &Option<String>,
) -> String {
"boot".to_string()
}
#[cfg(target_os = "android")]
pub fn get_slot_suffix(ota: bool) -> String {
let mut slot_suffix = utils::getprop("ro.boot.slot_suffix").unwrap_or_else(|| String::from(""));
if !slot_suffix.is_empty() && ota {
if slot_suffix == "_a" {
slot_suffix = "_b".to_string()
} else {
slot_suffix = "_a".to_string()
}
}
slot_suffix
}
#[cfg(not(target_os = "android"))]
pub fn get_slot_suffix(_ota: bool) -> String {
String::new()
}
#[cfg(target_os = "android")]
pub fn list_available_partitions() -> Vec<String> {
let slot_suffix = get_slot_suffix(false);
let candidates = vec!["boot", "init_boot", "vendor_boot"];
candidates
.into_iter()
.filter(|name| Path::new(&format!("/dev/block/by-name/{}{}", name, slot_suffix)).exists())
.map(|s| s.to_string())
.collect()
}
#[cfg(not(target_os = "android"))]
pub fn list_available_partitions() -> Vec<String> {
Vec::new()
}
fn post_ota() -> Result<()> {
use crate::defs::ADB_DIR;
use assets::BOOTCTL_PATH;
let status = Command::new(BOOTCTL_PATH).arg("hal-info").status()?;
if !status.success() {
return Ok(());
}
let current_slot = Command::new(BOOTCTL_PATH)
.arg("get-current-slot")
.output()?
.stdout;
let current_slot = String::from_utf8(current_slot)?;
let current_slot = current_slot.trim();
let target_slot = if current_slot == "0" { 1 } else { 0 };
Command::new(BOOTCTL_PATH)
.arg(format!("set-active-boot-slot {target_slot}"))
.status()?;
let post_fs_data = std::path::Path::new(ADB_DIR).join("post-fs-data.d");
utils::ensure_dir_exists(&post_fs_data)?;
let post_ota_sh = post_fs_data.join("post_ota.sh");
let sh_content = format!(
r###"
{BOOTCTL_PATH} mark-boot-successful
rm -f {BOOTCTL_PATH}
rm -f /data/adb/post-fs-data.d/post_ota.sh
"###
);
std::fs::write(&post_ota_sh, sh_content)?;
#[cfg(unix)]
std::fs::set_permissions(post_ota_sh, std::fs::Permissions::from_mode(0o755))?;
Ok(())
}

771
userspace/ksud/src/cli.rs Normal file
View file

@ -0,0 +1,771 @@
use anyhow::{Ok, Result};
use clap::Parser;
use std::path::PathBuf;
#[cfg(target_os = "android")]
use android_logger::Config;
#[cfg(target_os = "android")]
use log::LevelFilter;
use crate::{apk_sign, assets, debug, defs, init_event, ksucalls, module, utils};
/// KernelSU userspace cli
#[derive(Parser, Debug)]
#[command(author, version = defs::VERSION_NAME, about, long_about = None)]
struct Args {
#[command(subcommand)]
command: Commands,
}
#[derive(clap::Subcommand, Debug)]
enum Commands {
/// Manage KernelSU modules
Module {
#[command(subcommand)]
command: Module,
},
/// Trigger `post-fs-data` event
PostFsData,
/// Trigger `service` event
Services,
/// Trigger `boot-complete` event
BootCompleted,
/// Install KernelSU userspace component to system
Install {
#[arg(long, default_value = None)]
magiskboot: Option<PathBuf>,
},
/// Uninstall KernelSU modules and itself(LKM Only)
Uninstall {
/// magiskboot path, if not specified, will search from $PATH
#[arg(long, default_value = None)]
magiskboot: Option<PathBuf>,
},
/// SELinux policy Patch tool
Sepolicy {
#[command(subcommand)]
command: Sepolicy,
},
/// Manage App Profiles
Profile {
#[command(subcommand)]
command: Profile,
},
/// Manage kernel features
Feature {
#[command(subcommand)]
command: Feature,
},
/// Patch boot or init_boot images to apply KernelSU
BootPatch {
/// boot image path, if not specified, will try to find the boot image automatically
#[arg(short, long)]
boot: Option<PathBuf>,
/// kernel image path to replace
#[arg(short, long)]
kernel: Option<PathBuf>,
/// LKM module path to replace, if not specified, will use the builtin one
#[arg(short, long)]
module: Option<PathBuf>,
/// init to be replaced
#[arg(short, long, requires("module"))]
init: Option<PathBuf>,
/// will use another slot when boot image is not specified
#[arg(short = 'u', long, default_value = "false")]
ota: bool,
/// Flash it to boot partition after patch
#[arg(short, long, default_value = "false")]
flash: bool,
/// output path, if not specified, will use current directory
#[arg(short, long, default_value = None)]
out: Option<PathBuf>,
/// magiskboot path, if not specified, will search from $PATH
#[arg(long, default_value = None)]
magiskboot: Option<PathBuf>,
/// KMI version, if specified, will use the specified KMI
#[arg(long, default_value = None)]
kmi: Option<String>,
/// target partition override (init_boot | boot | vendor_boot)
#[arg(long, default_value = None)]
partition: Option<String>,
},
/// Restore boot or init_boot images patched by KernelSU
BootRestore {
/// boot image path, if not specified, will try to find the boot image automatically
#[arg(short, long)]
boot: Option<PathBuf>,
/// Flash it to boot partition after patch
#[arg(short, long, default_value = "false")]
flash: bool,
/// magiskboot path, if not specified, will search from $PATH
#[arg(long, default_value = None)]
magiskboot: Option<PathBuf>,
},
/// Show boot information
BootInfo {
#[command(subcommand)]
command: BootInfo,
},
/// KPM module manager
#[cfg(target_arch = "aarch64")]
Kpm {
#[command(subcommand)]
command: kpm_cmd::Kpm,
},
/// Manage kernel umount paths
Umount {
#[command(subcommand)]
command: Umount,
},
/// For developers
Debug {
#[command(subcommand)]
command: Debug,
},
/// Kernel interface
Kernel {
#[command(subcommand)]
command: Kernel,
},
}
#[derive(clap::Subcommand, Debug)]
enum BootInfo {
/// show current kmi version
CurrentKmi,
/// show supported kmi versions
SupportedKmis,
/// check if device is A/B capable
IsAbDevice,
/// show auto-selected boot partition name
DefaultPartition,
/// list available partitions for current or OTA toggled slot
AvailablePartitions,
/// show slot suffix for current or OTA toggled slot
SlotSuffix {
/// toggle to another slot
#[arg(short = 'u', long, default_value = "false")]
ota: bool,
},
}
#[derive(clap::Subcommand, Debug)]
enum Debug {
/// Set the manager app, kernel CONFIG_KSU_DEBUG should be enabled.
SetManager {
/// manager package name
#[arg(default_value_t = String::from("com.sukisu.ultra"))]
apk: String,
},
/// Get apk size and hash
GetSign {
/// apk path
apk: String,
},
/// Root Shell
Su {
/// switch to gloabl mount namespace
#[arg(short, long, default_value = "false")]
global_mnt: bool,
},
/// Get kernel version
Version,
/// For testing
Test,
/// Process mark management
Mark {
#[command(subcommand)]
command: MarkCommand,
},
}
#[derive(clap::Subcommand, Debug)]
enum MarkCommand {
/// Get mark status for a process (or all)
Get {
/// target pid (0 for total count)
#[arg(default_value = "0")]
pid: i32,
},
/// Mark a process
Mark {
/// target pid (0 for all processes)
#[arg(default_value = "0")]
pid: i32,
},
/// Unmark a process
Unmark {
/// target pid (0 for all processes)
#[arg(default_value = "0")]
pid: i32,
},
/// Refresh mark for all running processes
Refresh,
}
#[derive(clap::Subcommand, Debug)]
enum Sepolicy {
/// Patch sepolicy
Patch {
/// sepolicy statements
sepolicy: String,
},
/// Apply sepolicy from file
Apply {
/// sepolicy file path
file: String,
},
/// Check if sepolicy statement is supported/valid
Check {
/// sepolicy statements
sepolicy: String,
},
}
#[derive(clap::Subcommand, Debug)]
enum Module {
/// Install module <ZIP>
Install {
/// module zip file path
zip: String,
},
/// Undo module uninstall mark <id>
UndoUninstall {
/// module id
id: String,
},
/// Uninstall module <id>
Uninstall {
/// module id
id: String,
},
/// enable module <id>
Enable {
/// module id
id: String,
},
/// disable module <id>
Disable {
// module id
id: String,
},
/// run action for module <id>
Action {
// module id
id: String,
},
/// list all modules
List,
/// manage module configuration
Config {
#[command(subcommand)]
command: ModuleConfigCmd,
},
}
#[derive(clap::Subcommand, Debug)]
enum ModuleConfigCmd {
/// Get a config value
Get {
/// config key
key: String,
},
/// Set a config value
Set {
/// config key
key: String,
/// config value
value: String,
/// use temporary config (cleared on reboot)
#[arg(short, long)]
temp: bool,
},
/// List all config entries
List,
/// Delete a config entry
Delete {
/// config key
key: String,
/// delete from temporary config
#[arg(short, long)]
temp: bool,
},
/// Clear all config entries
Clear {
/// clear temporary config
#[arg(short, long)]
temp: bool,
},
}
#[derive(clap::Subcommand, Debug)]
enum Profile {
/// get root profile's selinux policy of <package-name>
GetSepolicy {
/// package name
package: String,
},
/// set root profile's selinux policy of <package-name> to <profile>
SetSepolicy {
/// package name
package: String,
/// policy statements
policy: String,
},
/// get template of <id>
GetTemplate {
/// template id
id: String,
},
/// set template of <id> to <template string>
SetTemplate {
/// template id
id: String,
/// template string
template: String,
},
/// delete template of <id>
DeleteTemplate {
/// template id
id: String,
},
/// list all templates
ListTemplates,
}
#[derive(clap::Subcommand, Debug)]
enum Feature {
/// Get feature value and support status
Get {
/// Feature ID or name (su_compat, kernel_umount)
id: String,
},
/// Set feature value
Set {
/// Feature ID or name
id: String,
/// Feature value (0=disable, 1=enable)
value: u64,
},
/// List all available features
List,
/// Check feature status (supported/unsupported/managed)
Check {
/// Feature ID or name (su_compat, kernel_umount)
id: String,
},
/// Load configuration from file and apply to kernel
Load,
/// Save current kernel feature states to file
Save,
}
#[derive(clap::Subcommand, Debug)]
enum Kernel {
/// Nuke ext4 sysfs
NukeExt4Sysfs {
/// mount point
mnt: String,
},
/// Manage umount list
Umount {
#[command(subcommand)]
command: UmountOp,
},
/// Notify that module is mounted
NotifyModuleMounted,
}
#[derive(clap::Subcommand, Debug)]
enum UmountOp {
/// Add mount point to umount list
Add {
/// mount point path
mnt: String,
/// umount flags (default: 0, MNT_DETACH: 2)
#[arg(short, long, default_value = "0")]
flags: u32,
},
/// Delete mount point from umount list
Del {
/// mount point path
mnt: String,
},
/// Wipe all entries from umount list
Wipe,
}
#[cfg(target_arch = "aarch64")]
mod kpm_cmd {
use clap::Subcommand;
use std::path::PathBuf;
#[derive(Subcommand, Debug)]
pub enum Kpm {
/// Load a KPM module: load <path> [args]
Load { path: PathBuf, args: Option<String> },
/// Unload a KPM module: unload <name>
Unload { name: String },
/// Get number of loaded modules
Num,
/// List loaded KPM modules
List,
/// Get info of a KPM module: info <name>
Info { name: String },
/// Send control command to a KPM module: control <name> <args>
Control { name: String, args: String },
/// Print KPM Loader version
Version,
}
}
#[derive(clap::Subcommand, Debug)]
enum Umount {
/// Add custom umount path
Add {
/// Mount path to add
path: String,
/// Check mount type (overlay)
#[arg(long, default_value = "false")]
/// Umount flags (0 or 8 for MNT_DETACH)
#[arg(long, default_value = "-1")]
flags: i32,
},
/// Remove custom umount path
Remove {
/// Mount path to remove
path: String,
},
/// List all umount paths
List,
/// Clear all recorded umount paths
ClearCustom,
/// Save configuration to file
Save,
/// Load and apply configuration from file
Load,
/// Apply current configuration to kernel
Apply,
}
pub fn run() -> Result<()> {
#[cfg(target_os = "android")]
android_logger::init_once(
Config::default()
.with_max_level(LevelFilter::Trace) // limit log level
.with_tag("KernelSU"), // logs will show under mytag tag
);
#[cfg(not(target_os = "android"))]
env_logger::init();
// the kernel executes su with argv[0] = "su" and replace it with us
let arg0 = std::env::args().next().unwrap_or_default();
if arg0 == "su" || arg0 == "/system/bin/su" {
return crate::su::root_shell();
}
let cli = Args::parse();
log::info!("command: {:?}", cli.command);
let result = match cli.command {
Commands::PostFsData => init_event::on_post_data_fs(),
Commands::BootCompleted => init_event::on_boot_completed(),
Commands::Module { command } => {
#[cfg(any(target_os = "linux", target_os = "android"))]
{
utils::switch_mnt_ns(1)?;
}
match command {
Module::Install { zip } => module::install_module(&zip),
Module::UndoUninstall { id } => module::undo_uninstall_module(&id),
Module::Uninstall { id } => module::uninstall_module(&id),
Module::Enable { id } => module::enable_module(&id),
Module::Disable { id } => module::disable_module(&id),
Module::Action { id } => module::run_action(&id),
Module::List => module::list_modules(),
Module::Config { command } => {
// Get module ID from environment variable
let module_id = std::env::var("KSU_MODULE").map_err(|_| {
anyhow::anyhow!("This command must be run in the context of a module")
})?;
use crate::module_config;
match command {
ModuleConfigCmd::Get { key } => {
// Use merge_configs to respect priority (temp overrides persist)
let config = module_config::merge_configs(&module_id)?;
match config.get(&key) {
Some(value) => {
println!("{}", value);
Ok(())
}
None => anyhow::bail!("Key '{}' not found", key),
}
}
ModuleConfigCmd::Set { key, value, temp } => {
// Validate input at CLI layer for better user experience
module_config::validate_config_key(&key)?;
module_config::validate_config_value(&value)?;
let config_type = if temp {
module_config::ConfigType::Temp
} else {
module_config::ConfigType::Persist
};
module_config::set_config_value(&module_id, &key, &value, config_type)
}
ModuleConfigCmd::List => {
let config = module_config::merge_configs(&module_id)?;
if config.is_empty() {
println!("No config entries found");
} else {
for (key, value) in config {
println!("{}={}", key, value);
}
}
Ok(())
}
ModuleConfigCmd::Delete { key, temp } => {
let config_type = if temp {
module_config::ConfigType::Temp
} else {
module_config::ConfigType::Persist
};
module_config::delete_config_value(&module_id, &key, config_type)
}
ModuleConfigCmd::Clear { temp } => {
let config_type = if temp {
module_config::ConfigType::Temp
} else {
module_config::ConfigType::Persist
};
module_config::clear_config(&module_id, config_type)
}
}
}
}
}
Commands::Install { magiskboot } => utils::install(magiskboot),
Commands::Uninstall { magiskboot } => utils::uninstall(magiskboot),
Commands::Sepolicy { command } => match command {
Sepolicy::Patch { sepolicy } => crate::sepolicy::live_patch(&sepolicy),
Sepolicy::Apply { file } => crate::sepolicy::apply_file(file),
Sepolicy::Check { sepolicy } => crate::sepolicy::check_rule(&sepolicy),
},
Commands::Services => init_event::on_services(),
Commands::Profile { command } => match command {
Profile::GetSepolicy { package } => crate::profile::get_sepolicy(package),
Profile::SetSepolicy { package, policy } => {
crate::profile::set_sepolicy(package, policy)
}
Profile::GetTemplate { id } => crate::profile::get_template(id),
Profile::SetTemplate { id, template } => crate::profile::set_template(id, template),
Profile::DeleteTemplate { id } => crate::profile::delete_template(id),
Profile::ListTemplates => crate::profile::list_templates(),
},
Commands::Feature { command } => match command {
Feature::Get { id } => crate::feature::get_feature(id),
Feature::Set { id, value } => crate::feature::set_feature(id, value),
Feature::List => crate::feature::list_features(),
Feature::Check { id } => crate::feature::check_feature(id),
Feature::Load => crate::feature::load_config_and_apply(),
Feature::Save => crate::feature::save_config(),
},
Commands::Debug { command } => match command {
Debug::SetManager { apk } => debug::set_manager(&apk),
Debug::GetSign { apk } => {
let sign = apk_sign::get_apk_signature(&apk)?;
println!("size: {:#x}, hash: {}", sign.0, sign.1);
Ok(())
}
Debug::Version => {
println!("Kernel Version: {}", ksucalls::get_version());
Ok(())
}
Debug::Su { global_mnt } => crate::su::grant_root(global_mnt),
Debug::Test => assets::ensure_binaries(false),
Debug::Mark { command } => match command {
MarkCommand::Get { pid } => debug::mark_get(pid),
MarkCommand::Mark { pid } => debug::mark_set(pid),
MarkCommand::Unmark { pid } => debug::mark_unset(pid),
MarkCommand::Refresh => debug::mark_refresh(),
},
},
Commands::BootPatch {
boot,
init,
kernel,
module,
ota,
flash,
out,
magiskboot,
kmi,
partition,
} => crate::boot_patch::patch(
boot, kernel, module, init, ota, flash, out, magiskboot, kmi, partition,
),
Commands::BootInfo { command } => match command {
BootInfo::CurrentKmi => {
let kmi = crate::boot_patch::get_current_kmi()?;
println!("{kmi}");
// return here to avoid printing the error message
return Ok(());
}
BootInfo::SupportedKmis => {
let kmi = crate::assets::list_supported_kmi()?;
kmi.iter().for_each(|kmi| println!("{kmi}"));
return Ok(());
}
BootInfo::IsAbDevice => {
let val = crate::utils::getprop("ro.build.ab_update")
.unwrap_or_else(|| String::from("false"));
let is_ab = val.trim().to_lowercase() == "true";
println!("{}", if is_ab { "true" } else { "false" });
return Ok(());
}
BootInfo::DefaultPartition => {
let kmi = crate::boot_patch::get_current_kmi().unwrap_or_else(|_| String::from(""));
let name = crate::boot_patch::choose_boot_partition(&kmi, false, &None);
println!("{name}");
return Ok(());
}
BootInfo::SlotSuffix { ota } => {
let suffix = crate::boot_patch::get_slot_suffix(ota);
println!("{suffix}");
return Ok(());
}
BootInfo::AvailablePartitions => {
let parts = crate::boot_patch::list_available_partitions();
parts.iter().for_each(|p| println!("{p}"));
return Ok(());
}
},
Commands::BootRestore {
boot,
magiskboot,
flash,
} => crate::boot_patch::restore(boot, magiskboot, flash),
Commands::Kernel { command } => match command {
Kernel::NukeExt4Sysfs { mnt } => ksucalls::nuke_ext4_sysfs(&mnt),
Kernel::Umount { command } => match command {
UmountOp::Add { mnt, flags } => ksucalls::umount_list_add(&mnt, flags),
UmountOp::Del { mnt } => ksucalls::umount_list_del(&mnt),
UmountOp::Wipe => ksucalls::umount_list_wipe().map_err(Into::into),
},
Kernel::NotifyModuleMounted => {
ksucalls::report_module_mounted();
Ok(())
}
},
#[cfg(target_arch = "aarch64")]
Commands::Kpm { command } => {
use crate::cli::kpm_cmd::Kpm;
match command {
Kpm::Load { path, args } => {
crate::kpm::kpm_load(path.to_str().unwrap(), args.as_deref())
}
Kpm::Unload { name } => crate::kpm::kpm_unload(&name),
Kpm::Num => crate::kpm::kpm_num().map(|_| ()),
Kpm::List => crate::kpm::kpm_list(),
Kpm::Info { name } => crate::kpm::kpm_info(&name),
Kpm::Control { name, args } => {
let ret = crate::kpm::kpm_control(&name, &args)?;
println!("{}", ret);
Ok(())
}
Kpm::Version => crate::kpm::kpm_version_loader(),
}
}
Commands::Umount { command } => match command {
Umount::Add { path, flags } => crate::umount_manager::add_umount_path(&path, flags),
Umount::Remove { path } => crate::umount_manager::remove_umount_path(&path),
Umount::List => crate::umount_manager::list_umount_paths(),
Umount::ClearCustom => crate::umount_manager::clear_custom_paths(),
Umount::Save => crate::umount_manager::save_umount_config(),
Umount::Load => crate::umount_manager::load_and_apply_config(),
Umount::Apply => crate::umount_manager::apply_config_to_kernel(),
},
};
if let Err(e) = &result {
log::error!("Error: {e:?}");
}
result
}

View file

@ -0,0 +1,98 @@
use anyhow::{Context, Ok, Result, bail, ensure};
use std::{
path::{Path, PathBuf},
process::Command,
};
use crate::ksucalls;
const KERNEL_PARAM_PATH: &str = "/sys/module/kernelsu";
fn read_u32(path: &PathBuf) -> Result<u32> {
let content = std::fs::read_to_string(path)?;
let content = content.trim();
let content = content.parse::<u32>()?;
Ok(content)
}
fn set_kernel_param(uid: u32) -> Result<()> {
let kernel_param_path = Path::new(KERNEL_PARAM_PATH).join("parameters");
let ksu_debug_manager_uid = kernel_param_path.join("ksu_debug_manager_uid");
let before_uid = read_u32(&ksu_debug_manager_uid)?;
std::fs::write(&ksu_debug_manager_uid, uid.to_string())?;
let after_uid = read_u32(&ksu_debug_manager_uid)?;
println!("set manager uid: {before_uid} -> {after_uid}");
Ok(())
}
#[cfg(target_os = "android")]
fn get_pkg_uid(pkg: &str) -> Result<u32> {
// stat /data/data/<pkg>
let uid = rustix::fs::stat(format!("/data/data/{pkg}"))
.with_context(|| format!("stat /data/data/{pkg}"))?
.st_uid;
Ok(uid)
}
pub fn set_manager(pkg: &str) -> Result<()> {
ensure!(
Path::new(KERNEL_PARAM_PATH).exists(),
"CONFIG_KSU_DEBUG is not enabled"
);
#[cfg(target_os = "android")]
let uid = get_pkg_uid(pkg)?;
#[cfg(not(target_os = "android"))]
let uid = 0;
set_kernel_param(uid)?;
// force-stop it
let _ = Command::new("am").args(["force-stop", pkg]).status();
Ok(())
}
/// Get mark status for a process
pub fn mark_get(pid: i32) -> Result<()> {
let result = ksucalls::mark_get(pid)?;
if pid == 0 {
bail!("Please specify a pid to get its mark status");
} else {
println!(
"Process {} mark status: {}",
pid,
if result != 0 { "marked" } else { "unmarked" }
);
}
Ok(())
}
/// Mark a process
pub fn mark_set(pid: i32) -> Result<()> {
ksucalls::mark_set(pid)?;
if pid == 0 {
println!("All processes marked successfully");
} else {
println!("Process {} marked successfully", pid);
}
Ok(())
}
/// Unmark a process
pub fn mark_unset(pid: i32) -> Result<()> {
ksucalls::mark_unset(pid)?;
if pid == 0 {
println!("All processes unmarked successfully");
} else {
println!("Process {} unmarked successfully", pid);
}
Ok(())
}
/// Refresh mark for all running processes
pub fn mark_refresh() -> Result<()> {
ksucalls::mark_refresh()?;
println!("Refreshed mark for all running processes");
Ok(())
}

View file

@ -0,0 +1,44 @@
use const_format::concatcp;
pub const ADB_DIR: &str = "/data/adb/";
pub const WORKING_DIR: &str = concatcp!(ADB_DIR, "ksu/");
pub const BINARY_DIR: &str = concatcp!(WORKING_DIR, "bin/");
pub const LOG_DIR: &str = concatcp!(WORKING_DIR, "log/");
pub const PROFILE_DIR: &str = concatcp!(WORKING_DIR, "profile/");
pub const PROFILE_SELINUX_DIR: &str = concatcp!(PROFILE_DIR, "selinux/");
pub const PROFILE_TEMPLATE_DIR: &str = concatcp!(PROFILE_DIR, "templates/");
pub const KSURC_PATH: &str = concatcp!(WORKING_DIR, ".ksurc");
pub const DAEMON_PATH: &str = concatcp!(ADB_DIR, "ksud");
pub const MAGISKBOOT_PATH: &str = concatcp!(BINARY_DIR, "magiskboot");
#[cfg(target_os = "android")]
pub const DAEMON_LINK_PATH: &str = concatcp!(BINARY_DIR, "ksud");
pub const MODULE_DIR: &str = concatcp!(ADB_DIR, "modules/");
pub const MODULE_UPDATE_DIR: &str = concatcp!(ADB_DIR, "modules_update/");
pub const METAMODULE_DIR: &str = concatcp!(ADB_DIR, "metamodule/");
pub const MODULE_WEB_DIR: &str = "webroot";
pub const MODULE_ACTION_SH: &str = "action.sh";
pub const DISABLE_FILE_NAME: &str = "disable";
pub const UPDATE_FILE_NAME: &str = "update";
pub const REMOVE_FILE_NAME: &str = "remove";
// Module config system
pub const MODULE_CONFIG_DIR: &str = concatcp!(WORKING_DIR, "module_configs/");
pub const PERSIST_CONFIG_NAME: &str = "persist.config";
pub const TEMP_CONFIG_NAME: &str = "tmp.config";
// Metamodule support
pub const METAMODULE_MOUNT_SCRIPT: &str = "metamount.sh";
pub const METAMODULE_METAINSTALL_SCRIPT: &str = "metainstall.sh";
pub const METAMODULE_METAUNINSTALL_SCRIPT: &str = "metauninstall.sh";
pub const VERSION_CODE: &str = include_str!(concat!(env!("OUT_DIR"), "/VERSION_CODE"));
pub const VERSION_NAME: &str = include_str!(concat!(env!("OUT_DIR"), "/VERSION_NAME"));
pub const KSU_BACKUP_DIR: &str = WORKING_DIR;
pub const KSU_BACKUP_FILE_PREFIX: &str = "ksu_backup_";
pub const BACKUP_FILENAME: &str = "stock_image.sha1";

View file

@ -0,0 +1,454 @@
use anyhow::{Context, Result, bail};
use const_format::concatcp;
use std::collections::HashMap;
use std::fs::File;
use std::io::{Read, Write};
use std::path::Path;
use crate::defs;
const FEATURE_CONFIG_PATH: &str = concatcp!(defs::WORKING_DIR, ".feature_config");
const FEATURE_MAGIC: u32 = 0x7f4b5355;
const FEATURE_VERSION: u32 = 1;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u32)]
pub enum FeatureId {
SuCompat = 0,
KernelUmount = 1,
EnhancedSecurity = 2,
SuLog = 3,
}
impl FeatureId {
pub fn from_u32(id: u32) -> Option<Self> {
match id {
0 => Some(FeatureId::SuCompat),
1 => Some(FeatureId::KernelUmount),
2 => Some(FeatureId::EnhancedSecurity),
3 => Some(FeatureId::SuLog),
_ => None,
}
}
pub fn name(&self) -> &'static str {
match self {
FeatureId::SuCompat => "su_compat",
FeatureId::KernelUmount => "kernel_umount",
FeatureId::EnhancedSecurity => "enhanced_security",
FeatureId::SuLog => "sulog",
}
}
pub fn description(&self) -> &'static str {
match self {
FeatureId::SuCompat => {
"SU Compatibility Mode - allows authorized apps to gain root via traditional 'su' command"
}
FeatureId::KernelUmount => {
"Kernel Umount - controls whether kernel automatically unmounts modules when not needed"
}
FeatureId::EnhancedSecurity => {
"Enhanced Security - disable nonKSU root elevation and unauthorized UID downgrades"
}
FeatureId::SuLog => {
"SU Log - enables logging of SU command usage to kernel log for auditing purposes"
}
}
}
}
fn parse_feature_id(name: &str) -> Result<FeatureId> {
match name {
"su_compat" | "0" => Ok(FeatureId::SuCompat),
"kernel_umount" | "1" => Ok(FeatureId::KernelUmount),
"enhanced_security" | "2" => Ok(FeatureId::EnhancedSecurity),
"sulog" | "3" => Ok(FeatureId::SuLog),
_ => bail!("Unknown feature: {}", name),
}
}
pub fn load_binary_config() -> Result<HashMap<u32, u64>> {
let path = Path::new(FEATURE_CONFIG_PATH);
if !path.exists() {
log::info!("Feature config not found, using defaults");
return Ok(HashMap::new());
}
let mut file = File::open(path).with_context(|| "Failed to open feature config")?;
let mut magic_buf = [0u8; 4];
file.read_exact(&mut magic_buf)
.with_context(|| "Failed to read magic")?;
let magic = u32::from_le_bytes(magic_buf);
if magic != FEATURE_MAGIC {
bail!(
"Invalid feature config magic: expected 0x{:08x}, got 0x{:08x}",
FEATURE_MAGIC,
magic
);
}
let mut version_buf = [0u8; 4];
file.read_exact(&mut version_buf)
.with_context(|| "Failed to read version")?;
let version = u32::from_le_bytes(version_buf);
if version != FEATURE_VERSION {
log::warn!(
"Feature config version mismatch: expected {}, got {}",
FEATURE_VERSION,
version
);
}
let mut count_buf = [0u8; 4];
file.read_exact(&mut count_buf)
.with_context(|| "Failed to read count")?;
let count = u32::from_le_bytes(count_buf);
let mut features = HashMap::new();
for _ in 0..count {
let mut id_buf = [0u8; 4];
let mut value_buf = [0u8; 8];
file.read_exact(&mut id_buf)
.with_context(|| "Failed to read feature id")?;
file.read_exact(&mut value_buf)
.with_context(|| "Failed to read feature value")?;
let id = u32::from_le_bytes(id_buf);
let value = u64::from_le_bytes(value_buf);
features.insert(id, value);
}
log::info!("Loaded {} features from config", features.len());
Ok(features)
}
pub fn save_binary_config(features: &HashMap<u32, u64>) -> Result<()> {
crate::utils::ensure_dir_exists(Path::new(defs::WORKING_DIR))?;
let path = Path::new(FEATURE_CONFIG_PATH);
let mut file = File::create(path).with_context(|| "Failed to create feature config")?;
file.write_all(&FEATURE_MAGIC.to_le_bytes())
.with_context(|| "Failed to write magic")?;
file.write_all(&FEATURE_VERSION.to_le_bytes())
.with_context(|| "Failed to write version")?;
let count = features.len() as u32;
file.write_all(&count.to_le_bytes())
.with_context(|| "Failed to write count")?;
for (&id, &value) in features.iter() {
file.write_all(&id.to_le_bytes())
.with_context(|| format!("Failed to write feature id {}", id))?;
file.write_all(&value.to_le_bytes())
.with_context(|| format!("Failed to write feature value for id {}", id))?;
}
file.sync_all()
.with_context(|| "Failed to sync feature config")?;
log::info!("Saved {} features to config", features.len());
Ok(())
}
pub fn apply_config(features: &HashMap<u32, u64>) -> Result<()> {
log::info!("Applying feature configuration to kernel...");
let mut applied = 0;
for (&id, &value) in features.iter() {
match crate::ksucalls::set_feature(id, value) {
Ok(_) => {
if let Some(feature_id) = FeatureId::from_u32(id) {
log::info!("Set feature {} to {}", feature_id.name(), value);
} else {
log::info!("Set feature {} to {}", id, value);
}
applied += 1;
}
Err(e) => {
log::warn!("Failed to set feature {}: {}", id, e);
}
}
}
log::info!("Applied {} features successfully", applied);
Ok(())
}
pub fn get_feature(id: String) -> Result<()> {
let feature_id = parse_feature_id(&id)?;
let (value, supported) = crate::ksucalls::get_feature(feature_id as u32)
.with_context(|| format!("Failed to get feature {}", id))?;
if !supported {
println!("Feature '{}' is not supported by kernel", id);
return Ok(());
}
println!("Feature: {} ({})", feature_id.name(), feature_id as u32);
println!("Description: {}", feature_id.description());
println!("Value: {}", value);
println!(
"Status: {}",
if value != 0 { "enabled" } else { "disabled" }
);
Ok(())
}
pub fn set_feature(id: String, value: u64) -> Result<()> {
let feature_id = parse_feature_id(&id)?;
// Check if this feature is managed by any module
if let Ok(managed_features_map) = crate::module::get_managed_features() {
// Find which modules manage this feature
let managing_modules: Vec<&String> = managed_features_map
.iter()
.filter(|(_, features)| features.iter().any(|f| f == feature_id.name()))
.map(|(module_id, _)| module_id)
.collect();
if !managing_modules.is_empty() {
// Feature is managed, check if caller is an authorized module
let caller_module = std::env::var("KSU_MODULE").unwrap_or_default();
if caller_module.is_empty() || !managing_modules.contains(&&caller_module) {
bail!(
"Feature '{}' is managed by module(s): {}. Direct modification is not allowed.",
feature_id.name(),
managing_modules
.iter()
.map(|s| s.as_str())
.collect::<Vec<_>>()
.join(", ")
);
}
log::info!(
"Module '{}' is setting managed feature '{}'",
caller_module,
feature_id.name()
);
}
}
crate::ksucalls::set_feature(feature_id as u32, value)
.with_context(|| format!("Failed to set feature {} to {}", id, value))?;
println!(
"Feature '{}' set to {} ({})",
feature_id.name(),
value,
if value != 0 { "enabled" } else { "disabled" }
);
Ok(())
}
pub fn list_features() -> Result<()> {
println!("Available Features:");
println!("{}", "=".repeat(80));
// Get managed features from modules
let managed_features_map = crate::module::get_managed_features().unwrap_or_default();
// Build a reverse map: feature_name -> Vec<module_id>
let mut feature_to_modules: HashMap<String, Vec<String>> = HashMap::new();
for (module_id, feature_list) in managed_features_map.iter() {
for feature_name in feature_list {
feature_to_modules
.entry(feature_name.clone())
.or_default()
.push(module_id.clone());
}
}
let all_features = [
FeatureId::SuCompat,
FeatureId::KernelUmount,
FeatureId::EnhancedSecurity,
FeatureId::SuLog,
];
for feature_id in all_features.iter() {
let id = *feature_id as u32;
let (value, supported) = crate::ksucalls::get_feature(id).unwrap_or((0, false));
let status = if !supported {
"NOT_SUPPORTED".to_string()
} else if value != 0 {
format!("ENABLED ({})", value)
} else {
"DISABLED".to_string()
};
let managed_by = feature_to_modules.get(feature_id.name());
let managed_mark = if managed_by.is_some() {
" [MODULE_MANAGED]"
} else {
""
};
println!(
"[{}] {} (ID={}){}",
status,
feature_id.name(),
id,
managed_mark
);
println!(" {}", feature_id.description());
if let Some(modules) = managed_by {
println!(
" ⚠️ Managed by module(s): {} (forced to 0 on initialization)",
modules.join(", ")
);
}
println!();
}
Ok(())
}
pub fn load_config_and_apply() -> Result<()> {
let features = load_binary_config()?;
if features.is_empty() {
println!("No features found in config file");
return Ok(());
}
apply_config(&features)?;
println!("Feature configuration loaded and applied");
Ok(())
}
pub fn save_config() -> Result<()> {
let mut features = HashMap::new();
let all_features = [
FeatureId::SuCompat,
FeatureId::KernelUmount,
FeatureId::EnhancedSecurity,
FeatureId::SuLog,
];
for feature_id in all_features.iter() {
let id = *feature_id as u32;
if let Ok((value, supported)) = crate::ksucalls::get_feature(id)
&& supported
{
features.insert(id, value);
log::info!("Saved feature {} = {}", feature_id.name(), value);
}
}
save_binary_config(&features)?;
println!(
"Current feature states saved to config file ({} features)",
features.len()
);
Ok(())
}
pub fn check_feature(id: String) -> Result<()> {
let feature_id = parse_feature_id(&id)?;
// Check if this feature is managed by any module
let managed_features_map = crate::module::get_managed_features().unwrap_or_default();
let is_managed = managed_features_map
.values()
.any(|features| features.iter().any(|f| f == feature_id.name()));
if is_managed {
println!("managed");
return Ok(());
}
// Check if the feature is supported by kernel
let (_value, supported) = crate::ksucalls::get_feature(feature_id as u32)
.with_context(|| format!("Failed to get feature {}", id))?;
if supported {
println!("supported");
} else {
println!("unsupported");
}
Ok(())
}
pub fn init_features() -> Result<()> {
log::info!("Initializing features from config...");
let mut features = load_binary_config()?;
// Get managed features from active modules and skip them during init
if let Ok(managed_features_map) = crate::module::get_managed_features() {
if !managed_features_map.is_empty() {
log::info!(
"Found {} modules managing features",
managed_features_map.len()
);
// Build a set of all managed feature IDs to skip
for (module_id, feature_list) in managed_features_map.iter() {
log::info!(
"Module '{}' manages {} feature(s)",
module_id,
feature_list.len()
);
for feature_name in feature_list {
if let Ok(feature_id) = parse_feature_id(feature_name) {
let feature_id_u32 = feature_id as u32;
// Remove managed features from config, let modules control them
if features.remove(&feature_id_u32).is_some() {
log::info!(
" - Skipping managed feature '{}' (controlled by module: {})",
feature_name,
module_id
);
} else {
log::info!(
" - Feature '{}' is managed by module '{}', skipping",
feature_name,
module_id
);
}
} else {
log::warn!(
" - Unknown managed feature '{}' from module '{}', ignoring",
feature_name,
module_id
);
}
}
}
}
} else {
log::warn!(
"Failed to get managed features from modules, continuing with normal initialization"
);
}
if features.is_empty() {
log::info!("No features to apply, skipping initialization");
return Ok(());
}
apply_config(&features)?;
// Save the configuration (excluding managed features)
save_binary_config(&features)?;
log::info!("Saved feature configuration to file");
Ok(())
}

View file

@ -0,0 +1,214 @@
#[cfg(target_arch = "aarch64")]
use crate::kpm;
use crate::module::{handle_updated_modules, prune_modules};
use crate::utils::is_safe_mode;
use crate::{
assets, defs, ksucalls, metamodule, restorecon,
utils::{self},
};
use anyhow::{Context, Result};
use log::{info, warn};
use std::path::Path;
pub fn on_post_data_fs() -> Result<()> {
ksucalls::report_post_fs_data();
utils::umask(0);
// Clear all temporary module configs early
if let Err(e) = crate::module_config::clear_all_temp_configs() {
warn!("clear temp configs failed: {e}");
}
#[cfg(unix)]
let _ = catch_bootlog("logcat", vec!["logcat"]);
#[cfg(unix)]
let _ = catch_bootlog("dmesg", vec!["dmesg", "-w"]);
if utils::has_magisk() {
warn!("Magisk detected, skip post-fs-data!");
return Ok(());
}
let safe_mode = crate::utils::is_safe_mode();
if safe_mode {
// we should still ensure module directory exists in safe mode
// because we may need to operate the module dir in safe mode
warn!("safe mode, skip common post-fs-data.d scripts");
} else {
// Then exec common post-fs-data scripts
if let Err(e) = crate::module::exec_common_scripts("post-fs-data.d", true) {
warn!("exec common post-fs-data scripts failed: {e}");
}
}
let module_dir = defs::MODULE_DIR;
assets::ensure_binaries(true).with_context(|| "Failed to extract bin assets")?;
// Start UID scanner daemon with highest priority
crate::uid_scanner::start_uid_scanner_daemon()?;
if is_safe_mode() {
warn!("safe mode, skip load feature config");
} else if let Err(e) = crate::umount_manager::load_and_apply_config() {
warn!("Failed to load umount config: {e}");
}
// if we are in safe mode, we should disable all modules
if safe_mode {
warn!("safe mode, skip post-fs-data scripts and disable all modules!");
if let Err(e) = crate::module::disable_all_modules() {
warn!("disable all modules failed: {e}");
}
return Ok(());
}
if let Err(e) = handle_updated_modules() {
warn!("handle updated modules failed: {e}");
}
if let Err(e) = prune_modules() {
warn!("prune modules failed: {e}");
}
if let Err(e) = restorecon::restorecon() {
warn!("restorecon failed: {e}");
}
// load sepolicy.rule
if crate::module::load_sepolicy_rule().is_err() {
warn!("load sepolicy.rule failed");
}
if let Err(e) = crate::profile::apply_sepolies() {
warn!("apply root profile sepolicy failed: {e}");
}
// load feature config
if is_safe_mode() {
warn!("safe mode, skip load feature config");
} else if let Err(e) = crate::feature::init_features() {
warn!("init features failed: {e}");
}
#[cfg(target_arch = "aarch64")]
if let Err(e) = kpm::start_kpm_watcher() {
warn!("KPM: Failed to start KPM watcher: {e}");
}
#[cfg(target_arch = "aarch64")]
if let Err(e) = kpm::load_kpm_modules() {
warn!("KPM: Failed to load KPM modules: {e}");
}
// execute metamodule post-fs-data script first (priority)
if let Err(e) = metamodule::exec_stage_script("post-fs-data", true) {
warn!("exec metamodule post-fs-data script failed: {e}");
}
// exec modules post-fs-data scripts
// TODO: Add timeout
if let Err(e) = crate::module::exec_stage_script("post-fs-data", true) {
warn!("exec post-fs-data scripts failed: {e}");
}
// load system.prop
if let Err(e) = crate::module::load_system_prop() {
warn!("load system.prop failed: {e}");
}
// execute metamodule mount script
if let Err(e) = metamodule::exec_mount_script(module_dir) {
warn!("execute metamodule mount failed: {e}");
}
run_stage("post-mount", true);
std::env::set_current_dir("/").with_context(|| "failed to chdir to /")?;
Ok(())
}
fn run_stage(stage: &str, block: bool) {
utils::umask(0);
if utils::has_magisk() {
warn!("Magisk detected, skip {stage}");
return;
}
if crate::utils::is_safe_mode() {
warn!("safe mode, skip {stage} scripts");
return;
}
if let Err(e) = crate::module::exec_common_scripts(&format!("{stage}.d"), block) {
warn!("Failed to exec common {stage} scripts: {e}");
}
// execute metamodule stage script first (priority)
if let Err(e) = metamodule::exec_stage_script(stage, block) {
warn!("Failed to exec metamodule {stage} script: {e}");
}
// execute regular modules stage scripts
if let Err(e) = crate::module::exec_stage_script(stage, block) {
warn!("Failed to exec {stage} scripts: {e}");
}
}
pub fn on_services() -> Result<()> {
info!("on_services triggered!");
run_stage("service", false);
Ok(())
}
pub fn on_boot_completed() -> Result<()> {
ksucalls::report_boot_complete();
info!("on_boot_completed triggered!");
run_stage("boot-completed", false);
Ok(())
}
#[cfg(unix)]
fn catch_bootlog(logname: &str, command: Vec<&str>) -> Result<()> {
use std::os::unix::process::CommandExt;
use std::process::Stdio;
let logdir = Path::new(defs::LOG_DIR);
utils::ensure_dir_exists(logdir)?;
let bootlog = logdir.join(format!("{logname}.log"));
let oldbootlog = logdir.join(format!("{logname}.old.log"));
if bootlog.exists() {
std::fs::rename(&bootlog, oldbootlog)?;
}
let bootlog = std::fs::File::create(bootlog)?;
let mut args = vec!["-s", "9", "30s"];
args.extend_from_slice(&command);
// timeout -s 9 30s logcat > boot.log
let result = unsafe {
std::process::Command::new("timeout")
.process_group(0)
.pre_exec(|| {
utils::switch_cgroups();
Ok(())
})
.args(args)
.stdout(Stdio::from(bootlog))
.spawn()
};
if let Err(e) = result {
warn!("Failed to start logcat: {e:#}");
}
Ok(())
}

View file

@ -0,0 +1,479 @@
#!/system/bin/sh
############################################
# KernelSU installer script
# mostly from module_installer.sh
# and util_functions.sh in Magisk
############################################
umask 022
ui_print() {
if $BOOTMODE; then
echo "$1"
else
echo -e "ui_print $1\nui_print" >> /proc/self/fd/$OUTFD
fi
}
toupper() {
echo "$@" | tr '[:lower:]' '[:upper:]'
}
grep_cmdline() {
local REGEX="s/^$1=//p"
{ echo $(cat /proc/cmdline)$(sed -e 's/[^"]//g' -e 's/""//g' /proc/cmdline) | xargs -n 1; \
sed -e 's/ = /=/g' -e 's/, /,/g' -e 's/"//g' /proc/bootconfig; \
} 2>/dev/null | sed -n "$REGEX"
}
grep_prop() {
local REGEX="s/$1=//p"
shift
local FILES=$@
[ -z "$FILES" ] && FILES='/system/build.prop'
cat $FILES 2>/dev/null | dos2unix | sed -n "$REGEX" | head -n 1 | xargs
}
grep_get_prop() {
local result=$(grep_prop $@)
if [ -z "$result" ]; then
# Fallback to getprop
getprop "$1"
else
echo $result
fi
}
is_mounted() {
grep -q " $(readlink -f $1) " /proc/mounts 2>/dev/null
return $?
}
abort() {
ui_print "$1"
$BOOTMODE || recovery_cleanup
[ ! -z $MODPATH ] && rm -rf $MODPATH
rm -rf $TMPDIR
exit 1
}
print_title() {
local len line1len line2len bar
line1len=$(echo -n $1 | wc -c)
line2len=$(echo -n $2 | wc -c)
len=$line2len
[ $line1len -gt $line2len ] && len=$line1len
len=$((len + 2))
bar=$(printf "%${len}s" | tr ' ' '*')
ui_print "$bar"
ui_print " $1 "
[ "$2" ] && ui_print " $2 "
ui_print "$bar"
}
check_sepolicy() {
/data/adb/ksud sepolicy check "$1"
return $?
}
######################
# Environment Related
######################
setup_flashable() {
ensure_bb
$BOOTMODE && return
if [ -z $OUTFD ] || readlink /proc/$$/fd/$OUTFD | grep -q /tmp; then
# We will have to manually find out OUTFD
for FD in `ls /proc/$$/fd`; do
if readlink /proc/$$/fd/$FD | grep -q pipe; then
if ps | grep -v grep | grep -qE " 3 $FD |status_fd=$FD"; then
OUTFD=$FD
break
fi
fi
done
fi
recovery_actions
}
ensure_bb() {
:
}
recovery_actions() {
:
}
recovery_cleanup() {
:
}
#######################
# Installation Related
#######################
# find_block [partname...]
find_block() {
local BLOCK DEV DEVICE DEVNAME PARTNAME UEVENT
for BLOCK in "$@"; do
DEVICE=`find /dev/block \( -type b -o -type c -o -type l \) -iname $BLOCK | head -n 1` 2>/dev/null
if [ ! -z $DEVICE ]; then
readlink -f $DEVICE
return 0
fi
done
# Fallback by parsing sysfs uevents
for UEVENT in /sys/dev/block/*/uevent; do
DEVNAME=`grep_prop DEVNAME $UEVENT`
PARTNAME=`grep_prop PARTNAME $UEVENT`
for BLOCK in "$@"; do
if [ "$(toupper $BLOCK)" = "$(toupper $PARTNAME)" ]; then
echo /dev/block/$DEVNAME
return 0
fi
done
done
# Look just in /dev in case we're dealing with MTD/NAND without /dev/block devices/links
for DEV in "$@"; do
DEVICE=`find /dev \( -type b -o -type c -o -type l \) -maxdepth 1 -iname $DEV | head -n 1` 2>/dev/null
if [ ! -z $DEVICE ]; then
readlink -f $DEVICE
return 0
fi
done
return 1
}
# setup_mntpoint <mountpoint>
setup_mntpoint() {
local POINT=$1
[ -L $POINT ] && mv -f $POINT ${POINT}_link
if [ ! -d $POINT ]; then
rm -f $POINT
mkdir -p $POINT
fi
}
# mount_name <partname(s)> <mountpoint> <flag>
mount_name() {
local PART=$1
local POINT=$2
local FLAG=$3
setup_mntpoint $POINT
is_mounted $POINT && return
# First try mounting with fstab
mount $FLAG $POINT 2>/dev/null
if ! is_mounted $POINT; then
local BLOCK=$(find_block $PART)
mount $FLAG $BLOCK $POINT || return
fi
ui_print "- Mounting $POINT"
}
# mount_ro_ensure <partname(s)> <mountpoint>
mount_ro_ensure() {
# We handle ro partitions only in recovery
$BOOTMODE && return
local PART=$1
local POINT=$2
mount_name "$PART" $POINT '-o ro'
is_mounted $POINT || abort "! Cannot mount $POINT"
}
mount_partitions() {
# Check A/B slot
SLOT=`grep_cmdline androidboot.slot_suffix`
if [ -z $SLOT ]; then
SLOT=`grep_cmdline androidboot.slot`
[ -z $SLOT ] || SLOT=_${SLOT}
fi
[ -z $SLOT ] || ui_print "- Current boot slot: $SLOT"
# Mount ro partitions
if is_mounted /system_root; then
umount /system 2&>/dev/null
umount /system_root 2&>/dev/null
fi
mount_ro_ensure "system$SLOT app$SLOT" /system
if [ -f /system/init -o -L /system/init ]; then
SYSTEM_ROOT=true
setup_mntpoint /system_root
if ! mount --move /system /system_root; then
umount /system
umount -l /system 2>/dev/null
mount_ro_ensure "system$SLOT app$SLOT" /system_root
fi
mount -o bind /system_root/system /system
else
SYSTEM_ROOT=false
grep ' / ' /proc/mounts | grep -qv 'rootfs' || grep -q ' /system_root ' /proc/mounts && SYSTEM_ROOT=true
fi
# /vendor is used only on some older devices for recovery AVBv1 signing so is not critical if fails
[ -L /system/vendor ] && mount_name vendor$SLOT /vendor '-o ro'
$SYSTEM_ROOT && ui_print "- Device is system-as-root"
# Mount sepolicy rules dir locations in recovery (best effort)
if ! $BOOTMODE; then
mount_name "cache cac" /cache
mount_name metadata /metadata
mount_name persist /persist
fi
}
api_level_arch_detect() {
API=$(grep_get_prop ro.build.version.sdk)
ABI=$(grep_get_prop ro.product.cpu.abi)
if [ "$ABI" = "x86" ]; then
ARCH=x86
ABI32=x86
IS64BIT=false
elif [ "$ABI" = "arm64-v8a" ]; then
ARCH=arm64
ABI32=armeabi-v7a
IS64BIT=true
elif [ "$ABI" = "x86_64" ]; then
ARCH=x64
ABI32=x86
IS64BIT=true
else
ARCH=arm
ABI=armeabi-v7a
ABI32=armeabi-v7a
IS64BIT=false
fi
}
#################
# Module Related
#################
check_managed_features() {
local PROP_FILE=$1
local MANAGED_FEATURES=$(grep_prop managedFeatures "$PROP_FILE")
[ -z "$MANAGED_FEATURES" ] && return 0
ui_print "- Checking managed features: $MANAGED_FEATURES"
# Split features by comma
echo "$MANAGED_FEATURES" | tr ',' '\n' | while read -r feature; do
# Trim whitespace
feature=$(echo "$feature" | xargs)
[ -z "$feature" ] && continue
# Check feature status using ksud
local status=$(/data/adb/ksud feature check "$feature" 2>/dev/null)
case "$status" in
"unsupported")
ui_print "! WARNING: Feature '$feature' is NOT SUPPORTED by kernel"
ui_print "! This module may not work correctly!"
;;
"managed")
ui_print "! WARNING: Feature '$feature' is already MANAGED by another module"
ui_print "! Feature conflicts may occur!"
;;
"supported")
ui_print "- Feature '$feature' is supported and available"
;;
*)
ui_print "! WARNING: Unable to check feature '$feature' status"
;;
esac
done
}
set_perm() {
chown $2:$3 $1 || return 1
chmod $4 $1 || return 1
local CON=$5
[ -z $CON ] && CON=u:object_r:system_file:s0
chcon $CON $1 || return 1
}
set_perm_recursive() {
find $1 -type d 2>/dev/null | while read dir; do
set_perm $dir $2 $3 $4 $6
done
find $1 -type f -o -type l 2>/dev/null | while read file; do
set_perm $file $2 $3 $5 $6
done
}
mktouch() {
mkdir -p ${1%/*} 2>/dev/null
[ -z $2 ] && touch $1 || echo $2 > $1
chmod 644 $1
}
mark_remove() {
mkdir -p ${1%/*} 2>/dev/null
mknod $1 c 0 0
chmod 644 $1
}
request_size_check() {
reqSizeM=`du -ms "$1" | cut -f1`
}
request_zip_size_check() {
reqSizeM=`unzip -l "$1" | tail -n 1 | awk '{ print int(($1 - 1) / 1048576 + 1) }'`
}
boot_actions() { return; }
# Require ZIPFILE to be set
is_legacy_script() {
unzip -l "$ZIPFILE" install.sh | grep -q install.sh
return $?
}
handle_partition() {
# if /system/vendor is a symlink, we need to move it out of $MODPATH/system
# if /system/vendor is a normal directory, no special handling is needed.
if [ ! -e $MODPATH/system/$1 ]; then
# no partition found
return;
fi
# we move the folder to / only if it is a native folder that is not a symlink
if [ -d "/$1" ] && [ ! -L "/$1" ]; then
ui_print "- Handle partition /$1"
# we create a symlink if module want to access $MODPATH/system/$1
# but it doesn't always work(ie. write it in post-fs-data.sh would fail because it is readonly)
mv -f $MODPATH/system/$1 $MODPATH/$1 && ln -sf ../$1 $MODPATH/system/$1
fi
}
# Require OUTFD, ZIPFILE to be set
install_module() {
rm -rf $TMPDIR
mkdir -p $TMPDIR
chcon u:object_r:system_file:s0 $TMPDIR
cd $TMPDIR
mount_partitions
api_level_arch_detect
# Setup busybox and binaries
if $BOOTMODE; then
boot_actions
else
recovery_actions
fi
# Extract prop file
unzip -o "$ZIPFILE" module.prop -d $TMPDIR >&2
[ ! -f $TMPDIR/module.prop ] && abort "! Unable to extract zip file!"
local MODDIRNAME=modules
$BOOTMODE && MODDIRNAME=modules_update
local MODULEROOT=$NVBASE/$MODDIRNAME
MODID=`grep_prop id $TMPDIR/module.prop`
MODNAME=`grep_prop name $TMPDIR/module.prop`
MODAUTH=`grep_prop author $TMPDIR/module.prop`
MODPATH=$MODULEROOT/$MODID
# Check managed features
check_managed_features $TMPDIR/module.prop
# Create mod paths
rm -rf $MODPATH
mkdir -p $MODPATH
if is_legacy_script; then
unzip -oj "$ZIPFILE" module.prop install.sh uninstall.sh 'common/*' -d $TMPDIR >&2
# Load install script
. $TMPDIR/install.sh
# Callbacks
print_modname
on_install
[ -f $TMPDIR/uninstall.sh ] && cp -af $TMPDIR/uninstall.sh $MODPATH/uninstall.sh
$SKIPMOUNT && touch $MODPATH/skip_mount
$PROPFILE && cp -af $TMPDIR/system.prop $MODPATH/system.prop
cp -af $TMPDIR/module.prop $MODPATH/module.prop
$POSTFSDATA && cp -af $TMPDIR/post-fs-data.sh $MODPATH/post-fs-data.sh
$LATESTARTSERVICE && cp -af $TMPDIR/service.sh $MODPATH/service.sh
ui_print "- Setting permissions"
set_permissions
else
print_title "$MODNAME" "by $MODAUTH"
print_title "Powered by KernelSU"
unzip -o "$ZIPFILE" customize.sh -d $MODPATH >&2
if ! grep -q '^SKIPUNZIP=1$' $MODPATH/customize.sh 2>/dev/null; then
ui_print "- Extracting module files"
unzip -o "$ZIPFILE" -x 'META-INF/*' -d $MODPATH >&2
# Default permissions
set_perm_recursive $MODPATH 0 0 0755 0644
set_perm_recursive $MODPATH/system/bin 0 2000 0755 0755
set_perm_recursive $MODPATH/system/xbin 0 2000 0755 0755
set_perm_recursive $MODPATH/system/system_ext/bin 0 2000 0755 0755
set_perm_recursive $MODPATH/system/vendor 0 2000 0755 0755 u:object_r:vendor_file:s0
fi
# Load customization script
[ -f $MODPATH/customize.sh ] && . $MODPATH/customize.sh
fi
# Handle replace folders
for TARGET in $REPLACE; do
ui_print "- Replace target: $TARGET"
mark_replace $MODPATH$TARGET
done
# Handle remove files
for TARGET in $REMOVE; do
ui_print "- Remove target: $TARGET"
mark_remove $MODPATH$TARGET
done
handle_partition vendor
handle_partition system_ext
handle_partition product
handle_partition odm
if $BOOTMODE; then
mktouch $NVBASE/modules/$MODID/update
rm -rf $NVBASE/modules/$MODID/remove 2>/dev/null
rm -rf $NVBASE/modules/$MODID/disable 2>/dev/null
cp -af $MODPATH/module.prop $NVBASE/modules/$MODID/module.prop
fi
# Remove stuff that doesn't belong to modules and clean up any empty directories
rm -rf \
$MODPATH/system/placeholder $MODPATH/customize.sh \
$MODPATH/README.md $MODPATH/.git*
rmdir -p $MODPATH 2>/dev/null
cd /
$BOOTMODE || recovery_cleanup
rm -rf $TMPDIR
ui_print "- Done"
}
##########
# Presets
##########
# Detect whether in boot mode
[ -z $BOOTMODE ] && ps | grep zygote | grep -qv grep && BOOTMODE=true
[ -z $BOOTMODE ] && ps -A 2>/dev/null | grep zygote | grep -qv grep && BOOTMODE=true
[ -z $BOOTMODE ] && BOOTMODE=false
NVBASE=/data/adb
TMPDIR=/dev/tmp
POSTFSDATAD=$NVBASE/post-fs-data.d
SERVICED=$NVBASE/service.d
# Some modules dependents on this
export MAGISK_VER=25.2
export MAGISK_VER_CODE=25200

331
userspace/ksud/src/kpm.rs Normal file
View file

@ -0,0 +1,331 @@
use std::{
ffi::{CStr, CString, OsStr},
fs,
os::unix::fs::PermissionsExt,
path::{Path, PathBuf},
};
use anyhow::{Result, bail};
use notify::{RecursiveMode, Watcher};
use crate::ksucalls::ksuctl;
pub const KPM_DIR: &str = "/data/adb/kpm";
const KPM_LOAD: u64 = 1;
const KPM_UNLOAD: u64 = 2;
const KPM_NUM: u64 = 3;
const KPM_LIST: u64 = 4;
const KPM_INFO: u64 = 5;
const KPM_CONTROL: u64 = 6;
const KPM_VERSION: u64 = 7;
const KSU_IOCTL_KPM: u32 = 0xc0004bc8; // _IOC(_IOC_READ|_IOC_WRITE, 'K', 200, 0)
#[repr(C)]
#[derive(Clone, Copy, Default)]
struct KsuKpmCmd {
pub control_code: u64,
pub arg1: u64,
pub arg2: u64,
pub result_code: u64,
}
fn kpm_ioctl(cmd: &mut KsuKpmCmd) -> std::io::Result<()> {
ksuctl(KSU_IOCTL_KPM, cmd as *mut _)?;
Ok(())
}
/// Convert raw kernel return code to `Result`.
fn check_ret(rc: i32) -> Result<i32> {
if rc < 0 {
bail!("KPM error: {}", std::io::Error::from_raw_os_error(-rc));
}
Ok(rc)
}
/// Load a `.kpm` into kernel space.
pub fn kpm_load<P>(path: P, args: Option<&str>) -> Result<()>
where
P: AsRef<Path>,
{
let path_c = CString::new(path.as_ref().to_string_lossy().to_string())?;
let args_c = args.map(CString::new).transpose()?;
let mut result: i32 = -1;
let mut cmd = KsuKpmCmd {
control_code: KPM_LOAD,
arg1: path_c.as_ptr() as u64,
arg2: args_c.as_ref().map_or(0, |s| s.as_ptr() as u64),
result_code: &mut result as *mut i32 as u64,
};
kpm_ioctl(&mut cmd)?;
check_ret(result)?;
println!("Success");
Ok(())
}
/// Unload by module name.
pub fn kpm_unload(name: &str) -> Result<()> {
let name_c = CString::new(name)?;
let mut result: i32 = -1;
let mut cmd = KsuKpmCmd {
control_code: KPM_UNLOAD,
arg1: name_c.as_ptr() as u64,
arg2: 0,
result_code: &mut result as *mut i32 as u64,
};
kpm_ioctl(&mut cmd)?;
check_ret(result)?;
Ok(())
}
/// Return loaded module count.
pub fn kpm_num() -> Result<i32> {
let mut result: i32 = -1;
let mut cmd = KsuKpmCmd {
control_code: KPM_NUM,
arg1: 0,
arg2: 0,
result_code: &mut result as *mut i32 as u64,
};
kpm_ioctl(&mut cmd)?;
let n = check_ret(result)?;
println!("{n}");
Ok(n)
}
/// Print name list of loaded modules.
pub fn kpm_list() -> Result<()> {
let mut buf = vec![0u8; 1024];
let mut result: i32 = -1;
let mut cmd = KsuKpmCmd {
control_code: KPM_LIST,
arg1: buf.as_mut_ptr() as u64,
arg2: buf.len() as u64,
result_code: &mut result as *mut i32 as u64,
};
kpm_ioctl(&mut cmd)?;
check_ret(result)?;
print!("{}", buf2str(&buf));
Ok(())
}
/// Print single module info.
pub fn kpm_info(name: &str) -> Result<()> {
let name_c = CString::new(name)?;
let mut buf = vec![0u8; 256];
let mut result: i32 = -1;
let mut cmd = KsuKpmCmd {
control_code: KPM_INFO,
arg1: name_c.as_ptr() as u64,
arg2: buf.as_mut_ptr() as u64,
result_code: &mut result as *mut i32 as u64,
};
kpm_ioctl(&mut cmd)?;
check_ret(result)?;
println!("{}", buf2str(&buf));
Ok(())
}
/// Send control string to a module; returns kernel answer.
pub fn kpm_control(name: &str, args: &str) -> Result<i32> {
let name_c = CString::new(name)?;
let args_c = CString::new(args)?;
let mut result: i32 = -1;
let mut cmd = KsuKpmCmd {
control_code: KPM_CONTROL,
arg1: name_c.as_ptr() as u64,
arg2: args_c.as_ptr() as u64,
result_code: &mut result as *mut i32 as u64,
};
kpm_ioctl(&mut cmd)?;
check_ret(result)
}
/// Print loader version string.
pub fn kpm_version_loader() -> Result<()> {
let mut buf = vec![0u8; 1024];
let mut result: i32 = -1;
let mut cmd = KsuKpmCmd {
control_code: KPM_VERSION,
arg1: buf.as_mut_ptr() as u64,
arg2: buf.len() as u64,
result_code: &mut result as *mut i32 as u64,
};
kpm_ioctl(&mut cmd)?;
check_ret(result)?;
print!("{}", buf2str(&buf));
Ok(())
}
/// Validate loader version; empty or "Error*" => fail.
pub fn check_kpm_version() -> Result<String> {
let mut buf = vec![0u8; 1024];
let mut result: i32 = -1;
let mut cmd = KsuKpmCmd {
control_code: KPM_VERSION,
arg1: buf.as_mut_ptr() as u64,
arg2: buf.len() as u64,
result_code: &mut result as *mut i32 as u64,
};
kpm_ioctl(&mut cmd)?;
check_ret(result)?;
let ver = buf2str(&buf);
if ver.is_empty() {
bail!("KPM: invalid version response: {ver}");
}
log::info!("KPM: version check ok: {ver}");
Ok(ver)
}
/// Create `/data/adb/kpm` with 0o777 if missing.
pub fn ensure_kpm_dir() -> Result<()> {
let _ = fs::create_dir_all(KPM_DIR);
let meta = fs::metadata(KPM_DIR)?;
if meta.permissions().mode() != 0o777 {
fs::set_permissions(KPM_DIR, fs::Permissions::from_mode(0o777))?;
}
Ok(())
}
/// Start file watcher for hot-(un)load.
pub fn start_kpm_watcher() -> Result<()> {
check_kpm_version()?; // bails if loader too old
ensure_kpm_dir()?;
if crate::utils::is_safe_mode() {
log::warn!("KPM: safe-mode removing all modules");
remove_all_kpms()?;
return Ok(());
}
let mut watcher = notify::recommended_watcher(|res: Result<_, _>| match res {
Ok(evt) => handle_kpm_event(evt),
Err(e) => log::error!("KPM: watcher error: {e}"),
})?;
watcher.watch(Path::new(KPM_DIR), RecursiveMode::NonRecursive)?;
log::info!("KPM: watcher active on {KPM_DIR}");
Ok(())
}
fn handle_kpm_event(evt: notify::Event) {
if let notify::EventKind::Create(_) = evt.kind {
for p in evt.paths {
if let Some(ex) = p.extension()
&& ex == OsStr::new("kpm")
&& kpm_load(&p, None).is_err()
{
log::warn!("KPM: failed to load {}", p.display());
}
}
}
}
/// Locate `/data/adb/kpm/<name>.kpm`.
fn find_kpm_file(name: &str) -> Result<Option<PathBuf>> {
let dir = Path::new(KPM_DIR);
if !dir.is_dir() {
return Ok(None);
}
for entry in fs::read_dir(dir)? {
let p = entry?.path();
if let Some(ex) = p.extension()
&& ex == OsStr::new("kpm")
&& let Some(fs) = p.file_stem()
&& fs == OsStr::new(name)
{
return Ok(Some(p));
}
}
Ok(None)
}
/// Remove every `.kpm` file and unload it.
pub fn remove_all_kpms() -> Result<()> {
let dir = Path::new(KPM_DIR);
if !dir.is_dir() {
return Ok(());
}
for entry in fs::read_dir(dir)? {
let p = entry?.path();
if let Some(ex) = p.extension()
&& ex == OsStr::new("kpm")
&& let Some(name) = p.file_stem().and_then(|s| s.to_str())
&& let Err(e) = (|| -> Result<()> {
kpm_unload(name)?;
if let Some(p) = find_kpm_file(name)? {
if let Err(e) = fs::remove_file(&p) {
log::warn!("KPM: delete {} failed: {e}", p.display());
return Err(e.into());
}
log::info!("KPM: deleted {}", p.display());
}
Ok(())
})()
{
log::error!("KPM: unload {name} failed: {e}");
}
}
Ok(())
}
/// Bulk-load existing `.kpm`s at boot.
pub fn load_kpm_modules() -> Result<()> {
check_kpm_version()?;
ensure_kpm_dir()?;
let dir = Path::new(KPM_DIR);
if !dir.is_dir() {
return Ok(());
}
let (mut ok, mut ng) = (0, 0);
for entry in fs::read_dir(dir)? {
let p = entry?.path();
if let Some(ex) = p.extension()
&& ex == OsStr::new("kpm")
{
match kpm_load(&p, None) {
Ok(_) => ok += 1,
Err(e) => {
log::warn!("KPM: load {} failed: {e}", p.display());
ng += 1;
}
}
}
}
log::info!("KPM: bulk-load done ok: {ok}, failed: {ng}");
Ok(())
}
/// Convert zero-padded kernel buffer to owned String.
fn buf2str(buf: &[u8]) -> String {
// SAFETY: buffer is always NUL-terminated by kernel.
unsafe {
CStr::from_ptr(buf.as_ptr().cast())
.to_string_lossy()
.into_owned()
}
}

View file

@ -0,0 +1,361 @@
use std::fs;
#[cfg(any(target_os = "linux", target_os = "android"))]
use std::os::fd::RawFd;
use std::sync::OnceLock;
// Event constants
const EVENT_POST_FS_DATA: u32 = 1;
const EVENT_BOOT_COMPLETED: u32 = 2;
const EVENT_MODULE_MOUNTED: u32 = 3;
const KSU_IOCTL_GRANT_ROOT: u32 = 0x00004b01; // _IOC(_IOC_NONE, 'K', 1, 0)
const KSU_IOCTL_GET_INFO: u32 = 0x80004b02; // _IOC(_IOC_READ, 'K', 2, 0)
const KSU_IOCTL_REPORT_EVENT: u32 = 0x40004b03; // _IOC(_IOC_WRITE, 'K', 3, 0)
const KSU_IOCTL_SET_SEPOLICY: u32 = 0xc0004b04; // _IOC(_IOC_READ|_IOC_WRITE, 'K', 4, 0)
const KSU_IOCTL_CHECK_SAFEMODE: u32 = 0x80004b05; // _IOC(_IOC_READ, 'K', 5, 0)
const KSU_IOCTL_GET_FEATURE: u32 = 0xc0004b0d; // _IOC(_IOC_READ|_IOC_WRITE, 'K', 13, 0)
const KSU_IOCTL_SET_FEATURE: u32 = 0x40004b0e; // _IOC(_IOC_WRITE, 'K', 14, 0)
const KSU_IOCTL_GET_WRAPPER_FD: u32 = 0x40004b0f; // _IOC(_IOC_WRITE, 'K', 15, 0)
const KSU_IOCTL_MANAGE_MARK: u32 = 0xc0004b10; // _IOC(_IOC_READ|_IOC_WRITE, 'K', 16, 0)
const KSU_IOCTL_NUKE_EXT4_SYSFS: u32 = 0x40004b11; // _IOC(_IOC_WRITE, 'K', 17, 0)
const KSU_IOCTL_ADD_TRY_UMOUNT: u32 = 0x40004b12; // _IOC(_IOC_WRITE, 'K', 18, 0)
#[repr(C)]
#[derive(Clone, Copy, Default)]
struct GetInfoCmd {
version: u32,
flags: u32,
}
#[repr(C)]
struct ReportEventCmd {
event: u32,
}
#[repr(C)]
#[derive(Clone, Copy, Default)]
pub struct SetSepolicyCmd {
pub cmd: u64,
pub arg: u64,
}
#[repr(C)]
#[derive(Clone, Copy, Default)]
struct CheckSafemodeCmd {
in_safe_mode: u8,
}
#[repr(C)]
#[derive(Clone, Copy, Default)]
struct GetFeatureCmd {
feature_id: u32,
value: u64,
supported: u8,
}
#[repr(C)]
#[derive(Clone, Copy, Default)]
struct SetFeatureCmd {
feature_id: u32,
value: u64,
}
#[repr(C)]
#[derive(Clone, Copy, Default)]
struct GetWrapperFdCmd {
fd: i32,
flags: u32,
}
#[repr(C)]
#[derive(Clone, Copy, Default)]
struct ManageMarkCmd {
operation: u32,
pid: i32,
result: u32,
}
#[repr(C)]
#[derive(Clone, Copy, Default)]
pub struct NukeExt4SysfsCmd {
pub arg: u64,
}
#[repr(C)]
#[derive(Clone, Copy, Default)]
struct AddTryUmountCmd {
arg: u64, // char ptr, this is the mountpoint
flags: u32, // this is the flag we use for it
mode: u8, // denotes what to do with it 0:wipe_list 1:add_to_list 2:delete_entry
}
// Mark operation constants
const KSU_MARK_GET: u32 = 1;
const KSU_MARK_MARK: u32 = 2;
const KSU_MARK_UNMARK: u32 = 3;
const KSU_MARK_REFRESH: u32 = 4;
// Umount operation constants
const KSU_UMOUNT_WIPE: u8 = 0;
const KSU_UMOUNT_ADD: u8 = 1;
const KSU_UMOUNT_DEL: u8 = 2;
// Global driver fd cache
#[cfg(any(target_os = "linux", target_os = "android"))]
static DRIVER_FD: OnceLock<RawFd> = OnceLock::new();
#[cfg(any(target_os = "linux", target_os = "android"))]
static INFO_CACHE: OnceLock<GetInfoCmd> = OnceLock::new();
const KSU_INSTALL_MAGIC1: u32 = 0xDEADBEEF;
const KSU_INSTALL_MAGIC2: u32 = 0xCAFEBABE;
#[cfg(any(target_os = "linux", target_os = "android"))]
fn scan_driver_fd() -> Option<RawFd> {
let fd_dir = fs::read_dir("/proc/self/fd").ok()?;
for entry in fd_dir.flatten() {
if let Ok(fd_num) = entry.file_name().to_string_lossy().parse::<i32>() {
let link_path = format!("/proc/self/fd/{}", fd_num);
if let Ok(target) = fs::read_link(&link_path) {
let target_str = target.to_string_lossy();
if target_str.contains("[ksu_driver]") {
return Some(fd_num);
}
}
}
}
None
}
// Get cached driver fd
#[cfg(any(target_os = "linux", target_os = "android"))]
fn init_driver_fd() -> Option<RawFd> {
let fd = scan_driver_fd();
if fd.is_none() {
let mut fd = -1;
unsafe {
libc::syscall(
libc::SYS_reboot,
KSU_INSTALL_MAGIC1,
KSU_INSTALL_MAGIC2,
0,
&mut fd,
);
};
if fd >= 0 { Some(fd) } else { None }
} else {
fd
}
}
// ioctl wrapper using libc
#[cfg(any(target_os = "linux", target_os = "android"))]
pub fn ksuctl<T>(request: u32, arg: *mut T) -> std::io::Result<i32> {
use std::io;
let fd = *DRIVER_FD.get_or_init(|| init_driver_fd().unwrap_or(-1));
unsafe {
#[cfg(not(target_env = "gnu"))]
let ret = libc::ioctl(fd as libc::c_int, request as i32, arg);
#[cfg(target_env = "gnu")]
let ret = libc::ioctl(fd as libc::c_int, request as u64, arg);
if ret < 0 {
Err(io::Error::last_os_error())
} else {
Ok(ret)
}
}
}
#[cfg(not(any(target_os = "linux", target_os = "android")))]
fn ksuctl<T>(_request: u32, _arg: *mut T) -> std::io::Result<i32> {
Err(std::io::Error::from_raw_os_error(libc::ENOSYS))
}
// API implementations
#[cfg(any(target_os = "linux", target_os = "android"))]
fn get_info() -> GetInfoCmd {
*INFO_CACHE.get_or_init(|| {
let mut cmd = GetInfoCmd {
version: 0,
flags: 0,
};
let _ = ksuctl(KSU_IOCTL_GET_INFO, &mut cmd as *mut _);
cmd
})
}
#[cfg(any(target_os = "linux", target_os = "android"))]
pub fn get_version() -> i32 {
get_info().version as i32
}
#[cfg(not(any(target_os = "linux", target_os = "android")))]
pub fn get_version() -> i32 {
0
}
#[cfg(any(target_os = "linux", target_os = "android"))]
pub fn grant_root() -> std::io::Result<()> {
ksuctl(KSU_IOCTL_GRANT_ROOT, std::ptr::null_mut::<u8>())?;
Ok(())
}
#[cfg(not(any(target_os = "linux", target_os = "android")))]
pub fn grant_root() -> std::io::Result<()> {
Err(std::io::Error::from_raw_os_error(libc::ENOSYS))
}
#[cfg(any(target_os = "linux", target_os = "android"))]
fn report_event(event: u32) {
let mut cmd = ReportEventCmd { event };
let _ = ksuctl(KSU_IOCTL_REPORT_EVENT, &mut cmd as *mut _);
}
#[cfg(not(any(target_os = "linux", target_os = "android")))]
fn report_event(_event: u32) {}
pub fn report_post_fs_data() {
report_event(EVENT_POST_FS_DATA);
}
pub fn report_boot_complete() {
report_event(EVENT_BOOT_COMPLETED);
}
pub fn report_module_mounted() {
report_event(EVENT_MODULE_MOUNTED);
}
#[cfg(any(target_os = "linux", target_os = "android"))]
pub fn check_kernel_safemode() -> bool {
let mut cmd = CheckSafemodeCmd { in_safe_mode: 0 };
let _ = ksuctl(KSU_IOCTL_CHECK_SAFEMODE, &mut cmd as *mut _);
cmd.in_safe_mode != 0
}
#[cfg(not(any(target_os = "linux", target_os = "android")))]
pub fn check_kernel_safemode() -> bool {
false
}
pub fn set_sepolicy(cmd: &SetSepolicyCmd) -> std::io::Result<()> {
let mut ioctl_cmd = *cmd;
ksuctl(KSU_IOCTL_SET_SEPOLICY, &mut ioctl_cmd as *mut _)?;
Ok(())
}
/// Get feature value and support status from kernel
/// Returns (value, supported)
pub fn get_feature(feature_id: u32) -> std::io::Result<(u64, bool)> {
let mut cmd = GetFeatureCmd {
feature_id,
value: 0,
supported: 0,
};
ksuctl(KSU_IOCTL_GET_FEATURE, &mut cmd as *mut _)?;
Ok((cmd.value, cmd.supported != 0))
}
/// Set feature value in kernel
pub fn set_feature(feature_id: u32, value: u64) -> std::io::Result<()> {
let mut cmd = SetFeatureCmd { feature_id, value };
ksuctl(KSU_IOCTL_SET_FEATURE, &mut cmd as *mut _)?;
Ok(())
}
#[cfg(any(target_os = "linux", target_os = "android"))]
pub fn get_wrapped_fd(fd: RawFd) -> std::io::Result<RawFd> {
let mut cmd = GetWrapperFdCmd { fd, flags: 0 };
let result = ksuctl(KSU_IOCTL_GET_WRAPPER_FD, &mut cmd as *mut _)?;
Ok(result)
}
/// Get mark status for a process (pid=0 returns total marked count)
pub fn mark_get(pid: i32) -> std::io::Result<u32> {
let mut cmd = ManageMarkCmd {
operation: KSU_MARK_GET,
pid,
result: 0,
};
ksuctl(KSU_IOCTL_MANAGE_MARK, &mut cmd as *mut _)?;
Ok(cmd.result)
}
/// Mark a process (pid=0 marks all processes)
pub fn mark_set(pid: i32) -> std::io::Result<()> {
let mut cmd = ManageMarkCmd {
operation: KSU_MARK_MARK,
pid,
result: 0,
};
ksuctl(KSU_IOCTL_MANAGE_MARK, &mut cmd as *mut _)?;
Ok(())
}
/// Unmark a process (pid=0 unmarks all processes)
pub fn mark_unset(pid: i32) -> std::io::Result<()> {
let mut cmd = ManageMarkCmd {
operation: KSU_MARK_UNMARK,
pid,
result: 0,
};
ksuctl(KSU_IOCTL_MANAGE_MARK, &mut cmd as *mut _)?;
Ok(())
}
/// Refresh mark for all running processes
pub fn mark_refresh() -> std::io::Result<()> {
let mut cmd = ManageMarkCmd {
operation: KSU_MARK_REFRESH,
pid: 0,
result: 0,
};
ksuctl(KSU_IOCTL_MANAGE_MARK, &mut cmd as *mut _)?;
Ok(())
}
pub fn nuke_ext4_sysfs(mnt: &str) -> anyhow::Result<()> {
let c_mnt = std::ffi::CString::new(mnt)?;
let mut ioctl_cmd = NukeExt4SysfsCmd {
arg: c_mnt.as_ptr() as u64,
};
ksuctl(KSU_IOCTL_NUKE_EXT4_SYSFS, &mut ioctl_cmd as *mut _)?;
Ok(())
}
/// Wipe all entries from umount list
pub fn umount_list_wipe() -> std::io::Result<()> {
let mut cmd = AddTryUmountCmd {
arg: 0,
flags: 0,
mode: KSU_UMOUNT_WIPE,
};
ksuctl(KSU_IOCTL_ADD_TRY_UMOUNT, &mut cmd as *mut _)?;
Ok(())
}
/// Add mount point to umount list
pub fn umount_list_add(path: &str, flags: u32) -> anyhow::Result<()> {
let c_path = std::ffi::CString::new(path)?;
let mut cmd = AddTryUmountCmd {
arg: c_path.as_ptr() as u64,
flags,
mode: KSU_UMOUNT_ADD,
};
ksuctl(KSU_IOCTL_ADD_TRY_UMOUNT, &mut cmd as *mut _)?;
Ok(())
}
/// Delete mount point from umount list
pub fn umount_list_del(path: &str) -> anyhow::Result<()> {
let c_path = std::ffi::CString::new(path)?;
let mut cmd = AddTryUmountCmd {
arg: c_path.as_ptr() as u64,
flags: 0,
mode: KSU_UMOUNT_DEL,
};
ksuctl(KSU_IOCTL_ADD_TRY_UMOUNT, &mut cmd as *mut _)?;
Ok(())
}

View file

@ -0,0 +1,26 @@
mod apk_sign;
mod assets;
mod boot_patch;
mod cli;
mod debug;
mod defs;
mod feature;
mod init_event;
#[cfg(target_arch = "aarch64")]
mod kpm;
mod ksucalls;
mod metamodule;
mod module;
mod module_config;
mod profile;
mod restorecon;
mod sepolicy;
mod su;
#[cfg(target_os = "android")]
mod uid_scanner;
mod umount_manager;
mod utils;
fn main() -> anyhow::Result<()> {
cli::run()
}

View file

@ -0,0 +1,287 @@
//! Metamodule management
//!
//! This module handles all metamodule-related functionality.
//! Metamodules are special modules that manage how regular modules are mounted
//! and provide hooks for module installation/uninstallation.
use anyhow::{Context, Result, ensure};
use log::{info, warn};
use std::{
collections::HashMap,
path::{Path, PathBuf},
process::Command,
};
use crate::module::ModuleType::All;
use crate::{assets, defs};
/// Determine whether the provided module properties mark it as a metamodule
pub fn is_metamodule(props: &HashMap<String, String>) -> bool {
props
.get("metamodule")
.map(|s| {
let trimmed = s.trim();
trimmed == "1" || trimmed.eq_ignore_ascii_case("true")
})
.unwrap_or(false)
}
/// Get metamodule path if it exists
/// The metamodule is stored in /data/adb/modules/{id} with a symlink at /data/adb/metamodule
pub fn get_metamodule_path() -> Option<PathBuf> {
let path = Path::new(defs::METAMODULE_DIR);
// Check if symlink exists and resolve it
if path.is_symlink()
&& let Ok(target) = std::fs::read_link(path)
{
// If target is relative, resolve it
let resolved = if target.is_absolute() {
target
} else {
path.parent()?.join(target)
};
if resolved.exists() && resolved.is_dir() {
return Some(resolved);
} else {
warn!(
"Metamodule symlink points to non-existent path: {:?}",
resolved
);
}
}
// Fallback: search for metamodule=1 in modules directory
let mut result = None;
let _ = crate::module::foreach_module(All, |module_path| {
if let Ok(props) = crate::module::read_module_prop(module_path)
&& is_metamodule(&props)
{
info!("Found metamodule in modules directory: {:?}", module_path);
result = Some(module_path.to_path_buf());
}
Ok(())
});
result
}
/// Check if metamodule exists
pub fn has_metamodule() -> bool {
get_metamodule_path().is_some()
}
/// Check if it's safe to install a regular module
/// Returns Ok(()) if safe, Err(is_disabled) if blocked
/// - Err(true) means metamodule is disabled
/// - Err(false) means metamodule is in other unstable state
pub fn check_install_safety() -> Result<(), bool> {
// No metamodule → safe
let Some(metamodule_path) = get_metamodule_path() else {
return Ok(());
};
// No metainstall.sh → safe (uses default installer)
// The staged update directory may contain the latest scripts, so check both locations
let has_metainstall = metamodule_path
.join(defs::METAMODULE_METAINSTALL_SCRIPT)
.exists()
|| metamodule_path.file_name().is_some_and(|module_id| {
Path::new(defs::MODULE_UPDATE_DIR)
.join(module_id)
.join(defs::METAMODULE_METAINSTALL_SCRIPT)
.exists()
});
if !has_metainstall {
return Ok(());
}
// Check for marker files
let has_update = metamodule_path.join(defs::UPDATE_FILE_NAME).exists();
let has_remove = metamodule_path.join(defs::REMOVE_FILE_NAME).exists();
let has_disable = metamodule_path.join(defs::DISABLE_FILE_NAME).exists();
// Stable state (no markers) → safe
if !has_update && !has_remove && !has_disable {
return Ok(());
}
// Return true if disabled, false for other unstable states
Err(has_disable && !has_update && !has_remove)
}
/// Create or update the metamodule symlink
/// Points /data/adb/metamodule -> /data/adb/modules/{module_id}
pub(crate) fn ensure_symlink(module_path: &Path) -> Result<()> {
// METAMODULE_DIR might have trailing slash, so we need to trim it
let symlink_path = Path::new(defs::METAMODULE_DIR.trim_end_matches('/'));
info!(
"Creating metamodule symlink: {:?} -> {:?}",
symlink_path, module_path
);
// Remove existing symlink if it exists
if symlink_path.exists() || symlink_path.is_symlink() {
info!("Removing old metamodule symlink/path");
if symlink_path.is_symlink() {
std::fs::remove_file(symlink_path).with_context(|| "Failed to remove old symlink")?;
} else {
// Could be a directory, remove it
std::fs::remove_dir_all(symlink_path)
.with_context(|| "Failed to remove old directory")?;
}
}
// Create symlink
#[cfg(unix)]
std::os::unix::fs::symlink(module_path, symlink_path)
.with_context(|| format!("Failed to create symlink to {:?}", module_path))?;
info!("Metamodule symlink created successfully");
Ok(())
}
/// Remove the metamodule symlink
pub(crate) fn remove_symlink() -> Result<()> {
let symlink_path = Path::new(defs::METAMODULE_DIR.trim_end_matches('/'));
if symlink_path.is_symlink() {
std::fs::remove_file(symlink_path)
.with_context(|| "Failed to remove metamodule symlink")?;
info!("Metamodule symlink removed");
}
Ok(())
}
/// Get the install script content, using metainstall.sh from metamodule if available
/// Returns the script content to be executed
pub(crate) fn get_install_script(
is_metamodule: bool,
installer_content: &str,
install_module_script: &str,
) -> Result<String> {
// Check if there's a metamodule with metainstall.sh
// Only apply this logic for regular modules (not when installing metamodule itself)
let install_script = if !is_metamodule {
if let Some(metamodule_path) = get_metamodule_path() {
if metamodule_path.join(defs::DISABLE_FILE_NAME).exists() {
info!("Metamodule is disabled, using default installer");
install_module_script.to_string()
} else {
let metainstall_path = metamodule_path.join(defs::METAMODULE_METAINSTALL_SCRIPT);
if metainstall_path.exists() {
info!("Using metainstall.sh from metamodule");
let metamodule_content = std::fs::read_to_string(&metainstall_path)
.with_context(|| "Failed to read metamodule metainstall.sh")?;
format!("{}\n{}\nexit 0\n", installer_content, metamodule_content)
} else {
info!("Metamodule exists but has no metainstall.sh, using default installer");
install_module_script.to_string()
}
}
} else {
info!("No metamodule found, using default installer");
install_module_script.to_string()
}
} else {
info!("Installing metamodule, using default installer");
install_module_script.to_string()
};
Ok(install_script)
}
/// Check if metamodule script exists and is ready to execute
/// Returns None if metamodule doesn't exist, is disabled, or script is missing
/// Returns Some(script_path) if script is ready to execute
fn check_metamodule_script(script_name: &str) -> Option<PathBuf> {
// Check if metamodule exists
let metamodule_path = get_metamodule_path()?;
// Check if metamodule is disabled
if metamodule_path.join(defs::DISABLE_FILE_NAME).exists() {
info!("Metamodule is disabled, skipping {}", script_name);
return None;
}
// Check if script exists
let script_path = metamodule_path.join(script_name);
if !script_path.exists() {
return None;
}
Some(script_path)
}
/// Execute metamodule's metauninstall.sh for a specific module
pub(crate) fn exec_metauninstall_script(module_id: &str) -> Result<()> {
let Some(metauninstall_path) = check_metamodule_script(defs::METAMODULE_METAUNINSTALL_SCRIPT)
else {
return Ok(());
};
info!(
"Executing metamodule metauninstall.sh for module: {}",
module_id
);
let result = Command::new(assets::BUSYBOX_PATH)
.args(["sh", metauninstall_path.to_str().unwrap()])
.current_dir(metauninstall_path.parent().unwrap())
.envs(crate::module::get_common_script_envs())
.env("MODULE_ID", module_id)
.status()?;
ensure!(
result.success(),
"Metamodule metauninstall.sh failed for module {}: {:?}",
module_id,
result
);
info!(
"Metamodule metauninstall.sh executed successfully for {}",
module_id
);
Ok(())
}
/// Execute metamodule mount script
pub fn exec_mount_script(module_dir: &str) -> Result<()> {
let Some(mount_script) = check_metamodule_script(defs::METAMODULE_MOUNT_SCRIPT) else {
return Ok(());
};
info!("Executing mount script for metamodule");
let result = Command::new(assets::BUSYBOX_PATH)
.args(["sh", mount_script.to_str().unwrap()])
.envs(crate::module::get_common_script_envs())
.env("MODULE_DIR", module_dir)
.status()?;
ensure!(
result.success(),
"Metamodule mount script failed with status: {:?}",
result
);
info!("Metamodule mount script executed successfully");
Ok(())
}
/// Execute metamodule script for a specific stage
pub fn exec_stage_script(stage: &str, block: bool) -> Result<()> {
let Some(script_path) = check_metamodule_script(&format!("{}.sh", stage)) else {
return Ok(());
};
info!("Executing metamodule {}.sh", stage);
crate::module::exec_script(&script_path, block)?;
info!("Metamodule {}.sh executed successfully", stage);
Ok(())
}

View file

@ -0,0 +1,855 @@
#[allow(clippy::wildcard_imports)]
use crate::utils::*;
use crate::{
assets, defs, ksucalls, metamodule,
restorecon::{restore_syscon, setsyscon},
sepolicy,
};
use anyhow::{Context, Result, anyhow, bail, ensure};
use const_format::concatcp;
use is_executable::is_executable;
use java_properties::PropertiesIter;
use log::{debug, info, warn};
use std::fs::{copy, rename};
use std::{
collections::HashMap,
env::var as env_var,
fs::{File, Permissions, canonicalize, remove_dir_all, set_permissions},
io::Cursor,
path::{Path, PathBuf},
process::Command,
str::FromStr,
};
use zip_extensions::zip_extract_file_to_memory;
use crate::defs::{MODULE_DIR, MODULE_UPDATE_DIR, UPDATE_FILE_NAME};
use crate::module::ModuleType::{Active, All};
#[cfg(unix)]
use std::os::unix::{prelude::PermissionsExt, process::CommandExt};
const INSTALLER_CONTENT: &str = include_str!("./installer.sh");
const INSTALL_MODULE_SCRIPT: &str = concatcp!(
INSTALLER_CONTENT,
"\n",
"install_module",
"\n",
"exit 0",
"\n"
);
/// Validate module_id format and security
/// Module ID must match: ^[a-zA-Z][a-zA-Z0-9._-]+$
/// - Must start with a letter (a-zA-Z)
/// - Followed by one or more alphanumeric, dot, underscore, or hyphen characters
/// - Minimum length: 2 characters
pub fn validate_module_id(module_id: &str) -> Result<()> {
if module_id.is_empty() {
bail!("Module ID cannot be empty");
}
if module_id.len() < 2 {
bail!("Module ID too short: must be at least 2 characters");
}
if module_id.len() > 64 {
bail!(
"Module ID too long: {} characters (max: 64)",
module_id.len()
);
}
// Check first character: must be a letter
let first_char = module_id.chars().next().unwrap();
if !first_char.is_ascii_alphabetic() {
bail!(
"Module ID must start with a letter (a-zA-Z), got: '{}'",
first_char
);
}
// Check remaining characters: alphanumeric, dot, underscore, or hyphen
for (i, ch) in module_id.chars().enumerate() {
if i == 0 {
continue; // Already checked
}
if !ch.is_ascii_alphanumeric() && ch != '.' && ch != '_' && ch != '-' {
bail!(
"Module ID contains invalid character '{}' at position {}. Only letters, digits, '.', '_', and '-' are allowed",
ch,
i
);
}
}
// Additional security checks
if module_id.contains("..") {
bail!("Module ID cannot contain '..' sequence");
}
if module_id == "." || module_id == ".." {
bail!("Module ID cannot be '.' or '..'");
}
Ok(())
}
/// Get common environment variables for script execution
pub(crate) fn get_common_script_envs() -> Vec<(&'static str, String)> {
vec![
("ASH_STANDALONE", "1".to_string()),
("KSU", "true".to_string()),
("KSU_KERNEL_VER_CODE", ksucalls::get_version().to_string()),
("KSU_VER_CODE", defs::VERSION_CODE.to_string()),
("KSU_VER", defs::VERSION_NAME.to_string()),
(
"PATH",
format!(
"{}:{}",
env_var("PATH").unwrap_or_default(),
defs::BINARY_DIR.trim_end_matches('/')
),
),
]
}
fn exec_install_script(module_file: &str, is_metamodule: bool) -> Result<()> {
let realpath = std::fs::canonicalize(module_file)
.with_context(|| format!("realpath: {module_file} failed"))?;
// Get install script from metamodule module
let install_script =
metamodule::get_install_script(is_metamodule, INSTALLER_CONTENT, INSTALL_MODULE_SCRIPT)?;
let result = Command::new(assets::BUSYBOX_PATH)
.args(["sh", "-c", &install_script])
.envs(get_common_script_envs())
.env("OUTFD", "1")
.env("ZIPFILE", realpath)
.status()?;
ensure!(result.success(), "Failed to install module script");
Ok(())
}
// Check if Android boot is completed before installing modules
fn ensure_boot_completed() -> Result<()> {
// ensure getprop sys.boot_completed == 1
if getprop("sys.boot_completed").as_deref() != Some("1") {
bail!("Android is Booting!");
}
Ok(())
}
#[derive(PartialEq, Eq)]
pub(crate) enum ModuleType {
All,
Active,
Updated,
}
pub(crate) fn foreach_module(
module_type: ModuleType,
mut f: impl FnMut(&Path) -> Result<()>,
) -> Result<()> {
let modules_dir = Path::new(match module_type {
ModuleType::Updated => MODULE_UPDATE_DIR,
_ => defs::MODULE_DIR,
});
let dir = std::fs::read_dir(modules_dir)?;
for entry in dir.flatten() {
let path = entry.path();
if !path.is_dir() {
warn!("{} is not a directory, skip", path.display());
continue;
}
if module_type == Active && path.join(defs::DISABLE_FILE_NAME).exists() {
info!("{} is disabled, skip", path.display());
continue;
}
if module_type == Active && path.join(defs::REMOVE_FILE_NAME).exists() {
warn!("{} is removed, skip", path.display());
continue;
}
f(&path)?;
}
Ok(())
}
fn foreach_active_module(f: impl FnMut(&Path) -> Result<()>) -> Result<()> {
foreach_module(Active, f)
}
pub fn load_sepolicy_rule() -> Result<()> {
foreach_active_module(|path| {
let rule_file = path.join("sepolicy.rule");
if !rule_file.exists() {
return Ok(());
}
info!("load policy: {}", &rule_file.display());
if sepolicy::apply_file(&rule_file).is_err() {
warn!("Failed to load sepolicy.rule for {}", &rule_file.display());
}
Ok(())
})?;
Ok(())
}
pub fn exec_script<T: AsRef<Path>>(path: T, wait: bool) -> Result<()> {
info!("exec {}", path.as_ref().display());
// Extract module_id from path if it matches /data/adb/modules/{id}/...
let module_id = path
.as_ref()
.strip_prefix(defs::MODULE_DIR)
.ok()
.and_then(|p| p.components().next())
.and_then(|c| c.as_os_str().to_str())
.map(|s| s.to_string());
// Validate and log module_id extraction
let validated_module_id = module_id
.as_ref()
.and_then(|id| match validate_module_id(id) {
Ok(_) => {
debug!("Module ID extracted from script path: '{}'", id);
Some(id.as_str())
}
Err(e) => {
warn!(
"Invalid module ID '{}' extracted from script path '{}': {}",
id,
path.as_ref().display(),
e
);
None
}
});
if module_id.is_none() {
debug!(
"Failed to extract module_id from script path '{}'. Script will run without KSU_MODULE environment variable.",
path.as_ref().display()
);
}
let mut command = &mut Command::new(assets::BUSYBOX_PATH);
#[cfg(unix)]
{
command = command.process_group(0);
command = unsafe {
command.pre_exec(|| {
// ignore the error?
switch_cgroups();
Ok(())
})
};
}
command = command
.current_dir(path.as_ref().parent().unwrap())
.arg("sh")
.arg(path.as_ref())
.envs(get_common_script_envs());
// Set KSU_MODULE environment variable if module_id was validated successfully
if let Some(id) = validated_module_id {
command = command.env("KSU_MODULE", id);
}
let result = if wait {
command.status().map(|_| ())
} else {
command.spawn().map(|_| ())
};
result.map_err(|err| anyhow!("Failed to exec {}: {}", path.as_ref().display(), err))
}
pub fn exec_stage_script(stage: &str, block: bool) -> Result<()> {
let metamodule_dir = metamodule::get_metamodule_path().and_then(|path| canonicalize(path).ok());
foreach_active_module(|module| {
if metamodule_dir.as_ref().is_some_and(|meta_dir| {
canonicalize(module)
.map(|resolved| resolved == *meta_dir)
.unwrap_or(false)
}) {
return Ok(());
}
let script_path = module.join(format!("{stage}.sh"));
if !script_path.exists() {
return Ok(());
}
exec_script(&script_path, block)
})?;
Ok(())
}
pub fn exec_common_scripts(dir: &str, wait: bool) -> Result<()> {
let script_dir = Path::new(defs::ADB_DIR).join(dir);
if !script_dir.exists() {
info!("{} not exists, skip", script_dir.display());
return Ok(());
}
let dir = std::fs::read_dir(&script_dir)?;
for entry in dir.flatten() {
let path = entry.path();
if !is_executable(&path) {
warn!("{} is not executable, skip", path.display());
continue;
}
exec_script(path, wait)?;
}
Ok(())
}
pub fn load_system_prop() -> Result<()> {
foreach_active_module(|module| {
let system_prop = module.join("system.prop");
if !system_prop.exists() {
return Ok(());
}
info!("load {} system.prop", module.display());
// resetprop -n --file system.prop
Command::new(assets::RESETPROP_PATH)
.arg("-n")
.arg("--file")
.arg(&system_prop)
.status()
.with_context(|| format!("Failed to exec {}", system_prop.display()))?;
Ok(())
})?;
Ok(())
}
pub fn prune_modules() -> Result<()> {
foreach_module(All, |module| {
if !module.join(defs::REMOVE_FILE_NAME).exists() {
return Ok(());
}
info!("remove module: {}", module.display());
// Execute metamodule's metauninstall.sh first
let module_id = module.file_name().and_then(|n| n.to_str()).unwrap_or("");
// Check if this is a metamodule
let is_metamodule = read_module_prop(module)
.map(|props| metamodule::is_metamodule(&props))
.unwrap_or(false);
if is_metamodule {
info!("Removing metamodule symlink");
if let Err(e) = metamodule::remove_symlink() {
warn!("Failed to remove metamodule symlink: {}", e);
}
} else if let Err(e) = metamodule::exec_metauninstall_script(module_id) {
warn!(
"Failed to exec metamodule uninstall for {}: {}",
module_id, e
);
}
// Then execute module's own uninstall.sh
let uninstaller = module.join("uninstall.sh");
if uninstaller.exists()
&& let Err(e) = exec_script(uninstaller, true)
{
warn!("Failed to exec uninstaller: {e}");
}
// Clear module configs before removing module directory
if let Err(e) = crate::module_config::clear_module_configs(module_id) {
warn!("Failed to clear configs for {}: {}", module_id, e);
}
// Finally remove the module directory
if let Err(e) = remove_dir_all(module) {
warn!("Failed to remove {}: {}", module.display(), e);
}
Ok(())
})?;
// collect remaining modules, if none, clean up metamodule record
let remaining_modules: Vec<_> = std::fs::read_dir(defs::MODULE_DIR)?
.filter_map(|entry| entry.ok())
.filter(|entry| entry.path().join("module.prop").exists())
.collect();
if remaining_modules.is_empty() {
info!("no remaining modules.");
}
Ok(())
}
pub fn handle_updated_modules() -> Result<()> {
let modules_root = Path::new(MODULE_DIR);
foreach_module(ModuleType::Updated, |updated_module| {
if !updated_module.is_dir() {
return Ok(());
}
if let Some(name) = updated_module.file_name() {
let module_dir = modules_root.join(name);
let mut disabled = false;
let mut removed = false;
if module_dir.exists() {
// If the old module is disabled, we need to also disable the new one
disabled = module_dir.join(defs::DISABLE_FILE_NAME).exists();
removed = module_dir.join(defs::REMOVE_FILE_NAME).exists();
remove_dir_all(&module_dir)?;
}
rename(updated_module, &module_dir)?;
if removed {
let path = module_dir.join(defs::REMOVE_FILE_NAME);
if let Err(e) = ensure_file_exists(&path) {
warn!("Failed to create {}: {}", path.display(), e);
}
} else if disabled {
let path = module_dir.join(defs::DISABLE_FILE_NAME);
if let Err(e) = ensure_file_exists(&path) {
warn!("Failed to create {}: {}", path.display(), e);
}
}
}
Ok(())
})?;
Ok(())
}
fn _install_module(zip: &str) -> Result<()> {
ensure_boot_completed()?;
// print banner
println!(include_str!("banner"));
assets::ensure_binaries(false).with_context(|| "Failed to extract assets")?;
// first check if working dir is usable
ensure_dir_exists(defs::WORKING_DIR).with_context(|| "Failed to create working dir")?;
ensure_dir_exists(defs::BINARY_DIR).with_context(|| "Failed to create bin dir")?;
// read the module_id from zip, if failed it will return early.
let mut buffer: Vec<u8> = Vec::new();
let entry_path = PathBuf::from_str("module.prop")?;
let zip_path = PathBuf::from_str(zip)?;
let zip_path = zip_path.canonicalize()?;
zip_extract_file_to_memory(&zip_path, &entry_path, &mut buffer)?;
let mut module_prop = HashMap::new();
PropertiesIter::new_with_encoding(Cursor::new(buffer), encoding_rs::UTF_8).read_into(
|k, v| {
module_prop.insert(k, v);
},
)?;
info!("module prop: {module_prop:?}");
let Some(module_id) = module_prop.get("id") else {
bail!("module id not found in module.prop!");
};
let module_id = module_id.trim();
// Validate module_id format
validate_module_id(module_id)
.with_context(|| format!("Invalid module ID in module.prop: '{}'", module_id))?;
// Check if this module is a metamodule
let is_metamodule = metamodule::is_metamodule(&module_prop);
// Check if it's safe to install regular module
if !is_metamodule && let Err(is_disabled) = metamodule::check_install_safety() {
println!("\n❌ Installation Blocked");
println!("┌────────────────────────────────");
println!("│ A metamodule with custom installer is active");
println!("");
if is_disabled {
println!("│ Current state: Disabled");
println!("│ Action required: Re-enable or uninstall it, then reboot");
} else {
println!("│ Current state: Pending changes");
println!("│ Action required: Reboot to apply changes first");
}
println!("└─────────────────────────────────\n");
bail!("Metamodule installation blocked");
}
// All modules (including metamodules) are installed to MODULE_UPDATE_DIR
let updated_dir = Path::new(defs::MODULE_UPDATE_DIR).join(module_id);
if is_metamodule {
info!("Installing metamodule: {}", module_id);
// Check if there's already a metamodule installed
if metamodule::has_metamodule()
&& let Some(existing_path) = metamodule::get_metamodule_path()
{
let existing_id = read_module_prop(&existing_path)
.ok()
.and_then(|m| m.get("id").cloned())
.unwrap_or_else(|| "unknown".to_string());
if existing_id != module_id {
println!("\n❌ Installation Failed");
println!("┌────────────────────────────────");
println!("│ A metamodule is already installed");
println!("│ Current metamodule: {}", existing_id);
println!("");
println!("│ Only one metamodule can be active at a time.");
println!("");
println!("│ To install this metamodule:");
println!("│ 1. Uninstall the current metamodule");
println!("│ 2. Reboot your device");
println!("│ 3. Install the new metamodule");
println!("└─────────────────────────────────\n");
bail!("Cannot install multiple metamodules");
}
}
}
let zip_uncompressed_size = get_zip_uncompressed_size(zip)?;
info!(
"zip uncompressed size: {}",
humansize::format_size(zip_uncompressed_size, humansize::DECIMAL)
);
println!(
"- Module size: {}",
humansize::format_size(zip_uncompressed_size, humansize::DECIMAL)
);
// Ensure module directory exists and set SELinux context
ensure_dir_exists(defs::MODULE_UPDATE_DIR)?;
setsyscon(defs::MODULE_UPDATE_DIR)?;
// Prepare target directory
println!("- Installing to {}", updated_dir.display());
ensure_clean_dir(&updated_dir)?;
info!("target dir: {}", updated_dir.display());
// Extract zip to target directory
println!("- Extracting module files");
let file = File::open(zip)?;
let mut archive = zip::ZipArchive::new(file)?;
archive.extract(&updated_dir)?;
// Set permission and selinux context for $MOD/system
let module_system_dir = updated_dir.join("system");
if module_system_dir.exists() {
#[cfg(unix)]
set_permissions(&module_system_dir, Permissions::from_mode(0o755))?;
restore_syscon(&module_system_dir)?;
}
// Execute install script
println!("- Running module installer");
exec_install_script(zip, is_metamodule)?;
let module_dir = Path::new(MODULE_DIR).join(module_id);
ensure_dir_exists(&module_dir)?;
copy(
updated_dir.join("module.prop"),
module_dir.join("module.prop"),
)?;
ensure_file_exists(module_dir.join(UPDATE_FILE_NAME))?;
// Create symlink for metamodule
if is_metamodule {
println!("- Creating metamodule symlink");
metamodule::ensure_symlink(&module_dir)?;
}
println!("- Module installed successfully!");
info!("Module {} installed successfully!", module_id);
Ok(())
}
pub fn install_module(zip: &str) -> Result<()> {
let result = _install_module(zip);
if let Err(ref e) = result {
println!("- Error: {e}");
}
result
}
pub fn undo_uninstall_module(id: &str) -> Result<()> {
validate_module_id(id)?;
let module_path = Path::new(defs::MODULE_DIR).join(id);
ensure!(module_path.exists(), "Module {} not found", id);
// Remove the remove mark
let remove_file = module_path.join(defs::REMOVE_FILE_NAME);
if remove_file.exists() {
std::fs::remove_file(&remove_file)
.with_context(|| format!("Failed to delete remove file for module '{}'", id))?;
info!("Removed the remove mark for module {}", id);
}
Ok(())
}
pub fn uninstall_module(id: &str) -> Result<()> {
validate_module_id(id)?;
let module_path = Path::new(defs::MODULE_DIR).join(id);
ensure!(module_path.exists(), "Module {} not found", id);
// Mark for removal
let remove_file = module_path.join(defs::REMOVE_FILE_NAME);
File::create(remove_file).with_context(|| "Failed to create remove file")?;
info!("Module {} marked for removal", id);
Ok(())
}
pub fn run_action(id: &str) -> Result<()> {
validate_module_id(id)?;
let action_script_path = format!("/data/adb/modules/{id}/action.sh");
exec_script(&action_script_path, true)
}
pub fn enable_module(id: &str) -> Result<()> {
validate_module_id(id)?;
let module_path = Path::new(defs::MODULE_DIR).join(id);
ensure!(module_path.exists(), "Module {} not found", id);
let disable_path = module_path.join(defs::DISABLE_FILE_NAME);
if disable_path.exists() {
std::fs::remove_file(&disable_path).with_context(|| {
format!("Failed to remove disable file: {}", disable_path.display())
})?;
info!("Module {} enabled", id);
}
Ok(())
}
pub fn disable_module(id: &str) -> Result<()> {
let module_path = Path::new(defs::MODULE_DIR).join(id);
ensure!(module_path.exists(), "Module {} not found", id);
let disable_path = module_path.join(defs::DISABLE_FILE_NAME);
ensure_file_exists(disable_path)?;
info!("Module {} disabled", id);
Ok(())
}
pub fn disable_all_modules() -> Result<()> {
mark_all_modules(defs::DISABLE_FILE_NAME)
}
pub fn uninstall_all_modules() -> Result<()> {
info!("Uninstalling all modules");
mark_all_modules(defs::REMOVE_FILE_NAME)
}
fn mark_all_modules(flag_file: &str) -> Result<()> {
// we assume the module dir is already mounted
let dir = std::fs::read_dir(defs::MODULE_DIR)?;
for entry in dir.flatten() {
let path = entry.path();
let flag = path.join(flag_file);
if let Err(e) = ensure_file_exists(flag) {
warn!("Failed to mark module: {}: {}", path.display(), e);
}
}
Ok(())
}
/// Read module.prop from the given module path and return as a HashMap
pub fn read_module_prop(module_path: &Path) -> Result<HashMap<String, String>> {
let module_prop = module_path.join("module.prop");
ensure!(
module_prop.exists(),
"module.prop not found in {}",
module_path.display()
);
let content = std::fs::read(&module_prop)
.with_context(|| format!("Failed to read module.prop: {}", module_prop.display()))?;
let mut prop_map: HashMap<String, String> = HashMap::new();
PropertiesIter::new_with_encoding(Cursor::new(content), encoding_rs::UTF_8)
.read_into(|k, v| {
prop_map.insert(k, v);
})
.with_context(|| format!("Failed to parse module.prop: {}", module_prop.display()))?;
Ok(prop_map)
}
fn _list_modules(path: &str) -> Vec<HashMap<String, String>> {
// Load all module configs once to minimize I/O overhead
let all_configs = match crate::module_config::get_all_module_configs() {
Ok(configs) => configs,
Err(e) => {
warn!("Failed to load module configs: {}", e);
HashMap::new()
}
};
// first check enabled modules
let dir = std::fs::read_dir(path);
let Ok(dir) = dir else {
return Vec::new();
};
let mut modules: Vec<HashMap<String, String>> = Vec::new();
for entry in dir.flatten() {
let path = entry.path();
info!("path: {}", path.display());
if !path.join("module.prop").exists() {
continue;
}
let mut module_prop_map = match read_module_prop(&path) {
Ok(prop) => prop,
Err(e) => {
warn!("Failed to read module.prop for {}: {}", path.display(), e);
continue;
}
};
// If id is missing or empty, use directory name as fallback
if !module_prop_map.contains_key("id") || module_prop_map["id"].is_empty() {
match entry.file_name().to_str() {
Some(id) => {
info!("Use dir name as module id: {id}");
module_prop_map.insert("id".to_owned(), id.to_owned());
}
_ => {
info!("Failed to get module id from dir name");
continue;
}
}
}
// Add enabled, update, remove, web, action flags
let enabled = !path.join(defs::DISABLE_FILE_NAME).exists();
let update = path.join(defs::UPDATE_FILE_NAME).exists();
let remove = path.join(defs::REMOVE_FILE_NAME).exists();
let web = path.join(defs::MODULE_WEB_DIR).exists();
let action = path.join(defs::MODULE_ACTION_SH).exists();
let need_mount = path.join("system").exists() && !path.join("skip_mount").exists();
module_prop_map.insert("enabled".to_owned(), enabled.to_string());
module_prop_map.insert("update".to_owned(), update.to_string());
module_prop_map.insert("remove".to_owned(), remove.to_string());
module_prop_map.insert("web".to_owned(), web.to_string());
module_prop_map.insert("action".to_owned(), action.to_string());
module_prop_map.insert("mount".to_owned(), need_mount.to_string());
// Apply module config overrides and extract managed features
if let Some(module_id) = module_prop_map.get("id")
&& let Some(config) = all_configs.get(module_id.as_str())
{
// Apply override.description
if let Some(desc) = config.get("override.description") {
module_prop_map.insert("description".to_owned(), desc.clone());
}
// Extract managed features from manage.* config entries
let managed_features: Vec<String> = config
.iter()
.filter_map(|(k, v)| {
if k.starts_with("manage.") && crate::module_config::parse_bool_config(v) {
k.strip_prefix("manage.").map(|f| f.to_string())
} else {
None
}
})
.collect();
if !managed_features.is_empty() {
module_prop_map.insert("managedFeatures".to_owned(), managed_features.join(","));
}
}
modules.push(module_prop_map);
}
modules
}
pub fn list_modules() -> Result<()> {
let modules = _list_modules(defs::MODULE_DIR);
println!("{}", serde_json::to_string_pretty(&modules)?);
Ok(())
}
/// Get all managed features from active modules
/// Modules declare managed features via config system (manage.<feature>=true)
/// Returns: HashMap<ModuleId, Vec<ManagedFeature>>
pub fn get_managed_features() -> Result<HashMap<String, Vec<String>>> {
let mut managed_features_map: HashMap<String, Vec<String>> = HashMap::new();
foreach_active_module(|module_path| {
// Get module ID
let module_id = match module_path.file_name().and_then(|n| n.to_str()) {
Some(id) => id,
None => {
warn!(
"Failed to get module id from path: {}",
module_path.display()
);
return Ok(());
}
};
// Read module config
let config = match crate::module_config::merge_configs(module_id) {
Ok(c) => c,
Err(e) => {
warn!("Failed to merge configs for module '{}': {}", module_id, e);
return Ok(()); // Skip this module
}
};
// Extract manage.* config entries
let mut feature_list = Vec::new();
for (key, value) in config.iter() {
if key.starts_with("manage.") {
// Parse feature name
if let Some(feature_name) = key.strip_prefix("manage.")
&& crate::module_config::parse_bool_config(value)
{
info!("Module {} manages feature: {}", module_id, feature_name);
feature_list.push(feature_name.to_string());
}
}
}
if !feature_list.is_empty() {
managed_features_map.insert(module_id.to_string(), feature_list);
}
Ok(())
})?;
Ok(managed_features_map)
}

View file

@ -0,0 +1,474 @@
use anyhow::{Context, Result, bail};
use log::{debug, warn};
use std::collections::HashMap;
use std::fs::{self, File};
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use crate::defs;
use crate::utils::ensure_dir_exists;
const MODULE_CONFIG_MAGIC: u32 = 0x4b53554d; // "KSUM"
const MODULE_CONFIG_VERSION: u32 = 1;
// Validation limits
pub const MAX_CONFIG_KEY_LEN: usize = 256;
pub const MAX_CONFIG_VALUE_LEN: usize = 256;
pub const MAX_CONFIG_COUNT: usize = 32;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConfigType {
Persist,
Temp,
}
impl ConfigType {
fn filename(&self) -> &'static str {
match self {
ConfigType::Persist => defs::PERSIST_CONFIG_NAME,
ConfigType::Temp => defs::TEMP_CONFIG_NAME,
}
}
}
/// Parse a boolean config value
/// Accepts "true", "1" (case-insensitive) as true, everything else as false
pub fn parse_bool_config(value: &str) -> bool {
let trimmed = value.trim();
trimmed.eq_ignore_ascii_case("true") || trimmed == "1"
}
/// Validate config key
/// Rejects keys with control characters, newlines, or excessive length
pub fn validate_config_key(key: &str) -> Result<()> {
if key.is_empty() {
bail!("Config key cannot be empty");
}
if key.len() > MAX_CONFIG_KEY_LEN {
bail!(
"Config key too long: {} bytes (max: {})",
key.len(),
MAX_CONFIG_KEY_LEN
);
}
// Check for control characters and newlines
for ch in key.chars() {
if ch.is_control() {
bail!(
"Config key contains control character: {:?} (U+{:04X})",
ch,
ch as u32
);
}
}
// Reject keys with path separators to prevent path traversal
if key.contains('/') || key.contains('\\') {
bail!("Config key cannot contain path separators");
}
Ok(())
}
/// Validate config value
/// Rejects values with control characters (except tab), newlines, or excessive length
pub fn validate_config_value(value: &str) -> Result<()> {
if value.len() > MAX_CONFIG_VALUE_LEN {
bail!(
"Config value too long: {} bytes (max: {})",
value.len(),
MAX_CONFIG_VALUE_LEN
);
}
// Check for control characters (allow tab but reject newlines and others)
for ch in value.chars() {
if ch.is_control() && ch != '\t' {
bail!(
"Config value contains invalid control character: {:?} (U+{:04X})",
ch,
ch as u32
);
}
}
Ok(())
}
/// Validate config count
fn validate_config_count(config: &HashMap<String, String>) -> Result<()> {
if config.len() > MAX_CONFIG_COUNT {
bail!(
"Too many config entries: {} (max: {})",
config.len(),
MAX_CONFIG_COUNT
);
}
Ok(())
}
/// Get the config directory path for a module
fn get_config_dir(module_id: &str) -> PathBuf {
Path::new(defs::MODULE_CONFIG_DIR).join(module_id)
}
/// Get the config file path for a module
fn get_config_path(module_id: &str, config_type: ConfigType) -> PathBuf {
get_config_dir(module_id).join(config_type.filename())
}
/// Ensure the config directory exists
fn ensure_config_dir(module_id: &str) -> Result<PathBuf> {
let dir = get_config_dir(module_id);
ensure_dir_exists(&dir)?;
Ok(dir)
}
/// Load config from binary file
pub fn load_config(module_id: &str, config_type: ConfigType) -> Result<HashMap<String, String>> {
crate::module::validate_module_id(module_id)?;
let config_path = get_config_path(module_id, config_type);
if !config_path.exists() {
debug!("Config file not found: {:?}", config_path);
return Ok(HashMap::new());
}
let mut file = File::open(&config_path)
.with_context(|| format!("Failed to open config file: {:?}", config_path))?;
// Read magic
let mut magic_buf = [0u8; 4];
file.read_exact(&mut magic_buf)
.with_context(|| "Failed to read magic")?;
let magic = u32::from_le_bytes(magic_buf);
if magic != MODULE_CONFIG_MAGIC {
bail!(
"Invalid config magic: expected 0x{:08x}, got 0x{:08x}",
MODULE_CONFIG_MAGIC,
magic
);
}
// Read version
let mut version_buf = [0u8; 4];
file.read_exact(&mut version_buf)
.with_context(|| "Failed to read version")?;
let version = u32::from_le_bytes(version_buf);
if version != MODULE_CONFIG_VERSION {
bail!(
"Unsupported config version: expected {}, got {}",
MODULE_CONFIG_VERSION,
version
);
}
// Read count
let mut count_buf = [0u8; 4];
file.read_exact(&mut count_buf)
.with_context(|| "Failed to read count")?;
let count = u32::from_le_bytes(count_buf);
// Read entries
let mut config = HashMap::new();
for i in 0..count {
// Read key length
let mut key_len_buf = [0u8; 4];
file.read_exact(&mut key_len_buf)
.with_context(|| format!("Failed to read key length for entry {}", i))?;
let key_len = u32::from_le_bytes(key_len_buf) as usize;
// Read key data
let mut key_buf = vec![0u8; key_len];
file.read_exact(&mut key_buf)
.with_context(|| format!("Failed to read key data for entry {}", i))?;
let key = String::from_utf8(key_buf)
.with_context(|| format!("Invalid UTF-8 in key for entry {}", i))?;
// Read value length
let mut value_len_buf = [0u8; 4];
file.read_exact(&mut value_len_buf)
.with_context(|| format!("Failed to read value length for entry {}", i))?;
let value_len = u32::from_le_bytes(value_len_buf) as usize;
// Read value data
let mut value_buf = vec![0u8; value_len];
file.read_exact(&mut value_buf)
.with_context(|| format!("Failed to read value data for entry {}", i))?;
let value = String::from_utf8(value_buf)
.with_context(|| format!("Invalid UTF-8 in value for entry {}", i))?;
config.insert(key, value);
}
debug!("Loaded {} entries from {:?}", config.len(), config_path);
Ok(config)
}
/// Save config to binary file
pub fn save_config(
module_id: &str,
config_type: ConfigType,
config: &HashMap<String, String>,
) -> Result<()> {
crate::module::validate_module_id(module_id)?;
// Validate config count
validate_config_count(config)?;
// Validate all keys and values
for (key, value) in config {
validate_config_key(key).with_context(|| format!("Invalid config key: '{}'", key))?;
validate_config_value(value)
.with_context(|| format!("Invalid config value for key '{}'", key))?;
}
ensure_config_dir(module_id)?;
let config_path = get_config_path(module_id, config_type);
let temp_path = config_path.with_extension("tmp");
// Write to temporary file first
let mut file = File::create(&temp_path)
.with_context(|| format!("Failed to create temp config file: {:?}", temp_path))?;
// Write magic
file.write_all(&MODULE_CONFIG_MAGIC.to_le_bytes())
.with_context(|| "Failed to write magic")?;
// Write version
file.write_all(&MODULE_CONFIG_VERSION.to_le_bytes())
.with_context(|| "Failed to write version")?;
// Write count
let count = config.len() as u32;
file.write_all(&count.to_le_bytes())
.with_context(|| "Failed to write count")?;
// Write entries
for (key, value) in config {
// Write key length
let key_bytes = key.as_bytes();
let key_len = key_bytes.len() as u32;
file.write_all(&key_len.to_le_bytes())
.with_context(|| format!("Failed to write key length for '{}'", key))?;
// Write key data
file.write_all(key_bytes)
.with_context(|| format!("Failed to write key data for '{}'", key))?;
// Write value length
let value_bytes = value.as_bytes();
let value_len = value_bytes.len() as u32;
file.write_all(&value_len.to_le_bytes())
.with_context(|| format!("Failed to write value length for '{}'", key))?;
// Write value data
file.write_all(value_bytes)
.with_context(|| format!("Failed to write value data for '{}'", key))?;
}
file.sync_all()
.with_context(|| "Failed to sync config file")?;
// Atomic rename
fs::rename(&temp_path, &config_path).with_context(|| {
format!(
"Failed to rename config file: {:?} -> {:?}",
temp_path, config_path
)
})?;
debug!("Saved {} entries to {:?}", config.len(), config_path);
Ok(())
}
/// Get a single config value
#[allow(dead_code)]
pub fn get_config_value(
module_id: &str,
key: &str,
config_type: ConfigType,
) -> Result<Option<String>> {
let config = load_config(module_id, config_type)?;
Ok(config.get(key).cloned())
}
/// Set a single config value
pub fn set_config_value(
module_id: &str,
key: &str,
value: &str,
config_type: ConfigType,
) -> Result<()> {
// Validate input early for better error messages
validate_config_key(key)?;
validate_config_value(value)?;
let mut config = load_config(module_id, config_type)?;
config.insert(key.to_string(), value.to_string());
// Note: save_config will also validate, but this provides earlier feedback
save_config(module_id, config_type, &config)?;
Ok(())
}
/// Delete a single config value
pub fn delete_config_value(module_id: &str, key: &str, config_type: ConfigType) -> Result<()> {
let mut config = load_config(module_id, config_type)?;
if config.remove(key).is_none() {
bail!("Key '{}' not found in config", key);
}
save_config(module_id, config_type, &config)?;
Ok(())
}
/// Clear all config values
pub fn clear_config(module_id: &str, config_type: ConfigType) -> Result<()> {
let config_path = get_config_path(module_id, config_type);
if config_path.exists() {
fs::remove_file(&config_path)
.with_context(|| format!("Failed to remove config file: {:?}", config_path))?;
debug!("Cleared config: {:?}", config_path);
}
Ok(())
}
/// Merge persist and temp configs (temp takes priority)
pub fn merge_configs(module_id: &str) -> Result<HashMap<String, String>> {
crate::module::validate_module_id(module_id)?;
let mut merged = match load_config(module_id, ConfigType::Persist) {
Ok(config) => config,
Err(e) => {
warn!(
"Failed to load persist config for module '{}': {}",
module_id, e
);
HashMap::new()
}
};
let temp = match load_config(module_id, ConfigType::Temp) {
Ok(config) => config,
Err(e) => {
warn!(
"Failed to load temp config for module '{}': {}",
module_id, e
);
HashMap::new()
}
};
// Temp config overrides persist config
for (key, value) in temp {
merged.insert(key, value);
}
Ok(merged)
}
/// Get all module configs (for iteration)
/// Loads all configs in a single pass to minimize I/O overhead
pub fn get_all_module_configs() -> Result<HashMap<String, HashMap<String, String>>> {
let config_root = Path::new(defs::MODULE_CONFIG_DIR);
if !config_root.exists() {
return Ok(HashMap::new());
}
let mut all_configs = HashMap::new();
for entry in fs::read_dir(config_root)
.with_context(|| format!("Failed to read config directory: {:?}", config_root))?
{
let entry = entry?;
let path = entry.path();
if !path.is_dir() {
continue;
}
if let Some(module_id) = path.file_name().and_then(|n| n.to_str()) {
match merge_configs(module_id) {
Ok(config) => {
if !config.is_empty() {
all_configs.insert(module_id.to_string(), config);
}
}
Err(e) => {
warn!("Failed to load config for module '{}': {}", module_id, e);
// Continue processing other modules
}
}
}
}
Ok(all_configs)
}
/// Clear all temporary configs (called during post-fs-data)
pub fn clear_all_temp_configs() -> Result<()> {
let config_root = Path::new(defs::MODULE_CONFIG_DIR);
if !config_root.exists() {
debug!("Config directory does not exist, nothing to clear");
return Ok(());
}
let mut cleared_count = 0;
for entry in fs::read_dir(config_root)
.with_context(|| format!("Failed to read config directory: {:?}", config_root))?
{
let entry = entry?;
let path = entry.path();
if !path.is_dir() {
continue;
}
let temp_config = path.join(defs::TEMP_CONFIG_NAME);
if temp_config.exists() {
match fs::remove_file(&temp_config) {
Ok(_) => {
debug!("Cleared temp config: {:?}", temp_config);
cleared_count += 1;
}
Err(e) => {
warn!("Failed to clear temp config {:?}: {}", temp_config, e);
}
}
}
}
if cleared_count > 0 {
debug!("Cleared {} temp config file(s)", cleared_count);
}
Ok(())
}
/// Clear all configs for a module (called during uninstall)
pub fn clear_module_configs(module_id: &str) -> Result<()> {
crate::module::validate_module_id(module_id)?;
let config_dir = get_config_dir(module_id);
if config_dir.exists() {
fs::remove_dir_all(&config_dir)
.with_context(|| format!("Failed to remove config directory: {:?}", config_dir))?;
debug!("Cleared all configs for module: {}", module_id);
}
Ok(())
}

View file

@ -0,0 +1,80 @@
use std::path::Path;
use anyhow::{Context, Result};
use crate::{defs, sepolicy, utils::ensure_dir_exists};
pub fn set_sepolicy(pkg: String, policy: String) -> Result<()> {
ensure_dir_exists(defs::PROFILE_SELINUX_DIR)?;
let policy_file = Path::new(defs::PROFILE_SELINUX_DIR).join(pkg);
std::fs::write(&policy_file, policy)?;
sepolicy::apply_file(&policy_file)?;
Ok(())
}
pub fn get_sepolicy(pkg: String) -> Result<()> {
let policy_file = Path::new(defs::PROFILE_SELINUX_DIR).join(pkg);
let policy = std::fs::read_to_string(policy_file)?;
println!("{policy}");
Ok(())
}
// ksud doesn't guarteen the correctness of template, it just save
pub fn set_template(id: String, template: String) -> Result<()> {
ensure_dir_exists(defs::PROFILE_TEMPLATE_DIR)?;
let template_file = Path::new(defs::PROFILE_TEMPLATE_DIR).join(id);
std::fs::write(template_file, template)?;
Ok(())
}
pub fn get_template(id: String) -> Result<()> {
let template_file = Path::new(defs::PROFILE_TEMPLATE_DIR).join(id);
let template = std::fs::read_to_string(template_file)?;
println!("{template}");
Ok(())
}
pub fn delete_template(id: String) -> Result<()> {
let template_file = Path::new(defs::PROFILE_TEMPLATE_DIR).join(id);
std::fs::remove_file(template_file)?;
Ok(())
}
pub fn list_templates() -> Result<()> {
let templates = std::fs::read_dir(defs::PROFILE_TEMPLATE_DIR);
let Ok(templates) = templates else {
return Ok(());
};
for template in templates {
let template = template?;
let template = template.file_name();
if let Some(template) = template.to_str() {
println!("{template}");
};
}
Ok(())
}
pub fn apply_sepolies() -> Result<()> {
let path = Path::new(defs::PROFILE_SELINUX_DIR);
if !path.exists() {
log::info!("profile sepolicy dir not exists.");
return Ok(());
}
let sepolicies =
std::fs::read_dir(path).with_context(|| "profile sepolicy dir open failed.".to_string())?;
for sepolicy in sepolicies {
let Ok(sepolicy) = sepolicy else {
log::info!("profile sepolicy dir read failed.");
continue;
};
let sepolicy = sepolicy.path();
if sepolicy::apply_file(&sepolicy).is_ok() {
log::info!("profile sepolicy applied: {sepolicy:?}");
} else {
log::info!("profile sepolicy apply failed: {sepolicy:?}");
}
}
Ok(())
}

View file

@ -0,0 +1,81 @@
use std::path::Path;
use anyhow::Result;
#[cfg(any(target_os = "linux", target_os = "android"))]
use anyhow::{Context, Ok};
#[cfg(any(target_os = "linux", target_os = "android"))]
use extattr::{Flags as XattrFlags, lsetxattr};
use jwalk::{Parallelism::Serial, WalkDir};
use crate::defs;
pub const SYSTEM_CON: &str = "u:object_r:system_file:s0";
pub const ADB_CON: &str = "u:object_r:adb_data_file:s0";
pub const UNLABEL_CON: &str = "u:object_r:unlabeled:s0";
const SELINUX_XATTR: &str = "security.selinux";
pub fn lsetfilecon<P: AsRef<Path>>(path: P, con: &str) -> Result<()> {
#[cfg(any(target_os = "linux", target_os = "android"))]
lsetxattr(&path, SELINUX_XATTR, con, XattrFlags::empty()).with_context(|| {
format!(
"Failed to change SELinux context for {}",
path.as_ref().display()
)
})?;
Ok(())
}
#[cfg(any(target_os = "linux", target_os = "android"))]
pub fn lgetfilecon<P: AsRef<Path>>(path: P) -> Result<String> {
let con = extattr::lgetxattr(&path, SELINUX_XATTR).with_context(|| {
format!(
"Failed to get SELinux context for {}",
path.as_ref().display()
)
})?;
let con = String::from_utf8_lossy(&con);
Ok(con.to_string())
}
#[cfg(any(target_os = "linux", target_os = "android"))]
pub fn setsyscon<P: AsRef<Path>>(path: P) -> Result<()> {
lsetfilecon(path, SYSTEM_CON)
}
#[cfg(not(any(target_os = "linux", target_os = "android")))]
pub fn setsyscon<P: AsRef<Path>>(path: P) -> Result<()> {
unimplemented!()
}
#[cfg(not(any(target_os = "linux", target_os = "android")))]
pub fn lgetfilecon<P: AsRef<Path>>(path: P) -> Result<String> {
unimplemented!()
}
pub fn restore_syscon<P: AsRef<Path>>(dir: P) -> Result<()> {
for dir_entry in WalkDir::new(dir).parallelism(Serial) {
if let Some(path) = dir_entry.ok().map(|dir_entry| dir_entry.path()) {
setsyscon(&path)?;
}
}
Ok(())
}
fn restore_syscon_if_unlabeled<P: AsRef<Path>>(dir: P) -> Result<()> {
for dir_entry in WalkDir::new(dir).parallelism(Serial) {
if let Some(path) = dir_entry.ok().map(|dir_entry| dir_entry.path())
&& let Result::Ok(con) = lgetfilecon(&path)
&& (con == UNLABEL_CON || con.is_empty())
{
lsetfilecon(&path, SYSTEM_CON)?;
}
}
Ok(())
}
pub fn restorecon() -> Result<()> {
lsetfilecon(defs::DAEMON_PATH, ADB_CON)?;
restore_syscon_if_unlabeled(defs::MODULE_DIR)?;
Ok(())
}

View file

@ -0,0 +1,744 @@
use std::{ffi, path::Path, vec};
use anyhow::{Result, bail};
use derive_new::new;
use nom::{
AsChar, IResult, Parser,
branch::alt,
bytes::complete::{tag, take_while, take_while_m_n, take_while1},
character::complete::{space0, space1},
combinator::map,
};
type SeObject<'a> = Vec<&'a str>;
fn is_sepolicy_char(c: char) -> bool {
c.is_alphanum() || c == '_' || c == '-'
}
fn parse_single_word(input: &str) -> IResult<&str, &str> {
take_while1(is_sepolicy_char).parse(input)
}
fn parse_bracket_objs(input: &str) -> IResult<&str, SeObject<'_>> {
let (input, (_, words, _)) = (
tag("{"),
take_while_m_n(1, 100, |c: char| is_sepolicy_char(c) || c.is_whitespace()),
tag("}"),
)
.parse(input)?;
Ok((input, words.split_whitespace().collect()))
}
fn parse_single_obj(input: &str) -> IResult<&str, SeObject<'_>> {
let (input, word) = take_while1(is_sepolicy_char).parse(input)?;
Ok((input, vec![word]))
}
fn parse_star(input: &str) -> IResult<&str, SeObject<'_>> {
let (input, _) = tag("*").parse(input)?;
Ok((input, vec!["*"]))
}
// 1. a single sepolicy word
// 2. { obj1 obj2 obj3 ...}
// 3. *
fn parse_seobj(input: &str) -> IResult<&str, SeObject<'_>> {
let (input, strs) = alt((parse_single_obj, parse_bracket_objs, parse_star)).parse(input)?;
Ok((input, strs))
}
fn parse_seobj_no_star(input: &str) -> IResult<&str, SeObject<'_>> {
let (input, strs) = alt((parse_single_obj, parse_bracket_objs)).parse(input)?;
Ok((input, strs))
}
trait SeObjectParser<'a> {
fn parse(input: &'a str) -> IResult<&'a str, Self>
where
Self: Sized;
}
#[derive(Debug, PartialEq, Eq, new)]
struct NormalPerm<'a> {
op: &'a str,
source: SeObject<'a>,
target: SeObject<'a>,
class: SeObject<'a>,
perm: SeObject<'a>,
}
#[derive(Debug, PartialEq, Eq, new)]
struct XPerm<'a> {
op: &'a str,
source: SeObject<'a>,
target: SeObject<'a>,
class: SeObject<'a>,
operation: &'a str,
perm_set: &'a str,
}
#[derive(Debug, PartialEq, Eq, new)]
struct TypeState<'a> {
op: &'a str,
stype: SeObject<'a>,
}
#[derive(Debug, PartialEq, Eq, new)]
struct TypeAttr<'a> {
stype: SeObject<'a>,
sattr: SeObject<'a>,
}
#[derive(Debug, PartialEq, Eq, new)]
struct Type<'a> {
name: &'a str,
attrs: SeObject<'a>,
}
#[derive(Debug, PartialEq, Eq, new)]
struct Attr<'a> {
name: &'a str,
}
#[derive(Debug, PartialEq, Eq, new)]
struct TypeTransition<'a> {
source: &'a str,
target: &'a str,
class: &'a str,
default_type: &'a str,
object_name: Option<&'a str>,
}
#[derive(Debug, PartialEq, Eq, new)]
struct TypeChange<'a> {
op: &'a str,
source: &'a str,
target: &'a str,
class: &'a str,
default_type: &'a str,
}
#[derive(Debug, PartialEq, Eq, new)]
struct GenFsCon<'a> {
fs_name: &'a str,
partial_path: &'a str,
fs_context: &'a str,
}
#[derive(Debug)]
enum PolicyStatement<'a> {
// "allow *source_type *target_type *class *perm_set"
// "deny *source_type *target_type *class *perm_set"
// "auditallow *source_type *target_type *class *perm_set"
// "dontaudit *source_type *target_type *class *perm_set"
NormalPerm(NormalPerm<'a>),
// "allowxperm *source_type *target_type *class operation xperm_set"
// "auditallowxperm *source_type *target_type *class operation xperm_set"
// "dontauditxperm *source_type *target_type *class operation xperm_set"
XPerm(XPerm<'a>),
// "permissive ^type"
// "enforce ^type"
TypeState(TypeState<'a>),
// "type type_name ^(attribute)"
Type(Type<'a>),
// "typeattribute ^type ^attribute"
TypeAttr(TypeAttr<'a>),
// "attribute ^attribute"
Attr(Attr<'a>),
// "type_transition source_type target_type class default_type (object_name)"
TypeTransition(TypeTransition<'a>),
// "type_change source_type target_type class default_type"
// "type_member source_type target_type class default_type"
TypeChange(TypeChange<'a>),
// "genfscon fs_name partial_path fs_context"
GenFsCon(GenFsCon<'a>),
}
impl<'a> SeObjectParser<'a> for NormalPerm<'a> {
fn parse(input: &'a str) -> IResult<&'a str, Self> {
let (input, op) = alt((
tag("allow"),
tag("deny"),
tag("auditallow"),
tag("dontaudit"),
))
.parse(input)?;
let (input, _) = space0(input)?;
let (input, source) = parse_seobj(input)?;
let (input, _) = space0(input)?;
let (input, target) = parse_seobj(input)?;
let (input, _) = space0(input)?;
let (input, class) = parse_seobj(input)?;
let (input, _) = space0(input)?;
let (input, perm) = parse_seobj(input)?;
Ok((input, NormalPerm::new(op, source, target, class, perm)))
}
}
impl<'a> SeObjectParser<'a> for XPerm<'a> {
fn parse(input: &'a str) -> IResult<&'a str, Self> {
let (input, op) = alt((
tag("allowxperm"),
tag("auditallowxperm"),
tag("dontauditxperm"),
))
.parse(input)?;
let (input, _) = space0(input)?;
let (input, source) = parse_seobj(input)?;
let (input, _) = space0(input)?;
let (input, target) = parse_seobj(input)?;
let (input, _) = space0(input)?;
let (input, class) = parse_seobj(input)?;
let (input, _) = space0(input)?;
let (input, operation) = parse_single_word(input)?;
let (input, _) = space0(input)?;
let (input, perm_set) = parse_single_word(input)?;
Ok((
input,
XPerm::new(op, source, target, class, operation, perm_set),
))
}
}
impl<'a> SeObjectParser<'a> for TypeState<'a> {
fn parse(input: &'a str) -> IResult<&'a str, Self> {
let (input, op) = alt((tag("permissive"), tag("enforce"))).parse(input)?;
let (input, _) = space1(input)?;
let (input, stype) = parse_seobj_no_star(input)?;
Ok((input, TypeState::new(op, stype)))
}
}
impl<'a> SeObjectParser<'a> for Type<'a> {
fn parse(input: &'a str) -> IResult<&'a str, Self> {
let (input, _) = tag("type")(input)?;
let (input, _) = space1(input)?;
let (input, name) = parse_single_word(input)?;
if input.is_empty() {
return Ok((input, Type::new(name, vec!["domain"]))); // default to domain
}
let (input, _) = space1(input)?;
let (input, attrs) = parse_seobj_no_star(input)?;
Ok((input, Type::new(name, attrs)))
}
}
impl<'a> SeObjectParser<'a> for TypeAttr<'a> {
fn parse(input: &'a str) -> IResult<&'a str, Self> {
let (input, _) = alt((tag("typeattribute"), tag("attradd"))).parse(input)?;
let (input, _) = space1(input)?;
let (input, stype) = parse_seobj_no_star(input)?;
let (input, _) = space1(input)?;
let (input, attr) = parse_seobj_no_star(input)?;
Ok((input, TypeAttr::new(stype, attr)))
}
}
impl<'a> SeObjectParser<'a> for Attr<'a> {
fn parse(input: &'a str) -> IResult<&'a str, Self> {
let (input, _) = tag("attribute")(input)?;
let (input, _) = space1(input)?;
let (input, attr) = parse_single_word(input)?;
Ok((input, Attr::new(attr)))
}
}
impl<'a> SeObjectParser<'a> for TypeTransition<'a> {
fn parse(input: &'a str) -> IResult<&'a str, Self> {
let (input, _) = alt((tag("type_transition"), tag("name_transition"))).parse(input)?;
let (input, _) = space1(input)?;
let (input, source) = parse_single_word(input)?;
let (input, _) = space1(input)?;
let (input, target) = parse_single_word(input)?;
let (input, _) = space1(input)?;
let (input, class) = parse_single_word(input)?;
let (input, _) = space1(input)?;
let (input, default) = parse_single_word(input)?;
if input.is_empty() {
return Ok((
input,
TypeTransition::new(source, target, class, default, None),
));
}
let (input, _) = space1(input)?;
let (input, object) = parse_single_word(input)?;
Ok((
input,
TypeTransition::new(source, target, class, default, Some(object)),
))
}
}
impl<'a> SeObjectParser<'a> for TypeChange<'a> {
fn parse(input: &'a str) -> IResult<&'a str, Self> {
let (input, op) = alt((tag("type_change"), tag("type_member"))).parse(input)?;
let (input, _) = space1(input)?;
let (input, source) = parse_single_word(input)?;
let (input, _) = space1(input)?;
let (input, target) = parse_single_word(input)?;
let (input, _) = space1(input)?;
let (input, class) = parse_single_word(input)?;
let (input, _) = space1(input)?;
let (input, default) = parse_single_word(input)?;
Ok((input, TypeChange::new(op, source, target, class, default)))
}
}
impl<'a> SeObjectParser<'a> for GenFsCon<'a> {
fn parse(input: &'a str) -> IResult<&'a str, Self>
where
Self: Sized,
{
let (input, _) = tag("genfscon")(input)?;
let (input, _) = space1(input)?;
let (input, fs) = parse_single_word(input)?;
let (input, _) = space1(input)?;
let (input, path) = parse_single_word(input)?;
let (input, _) = space1(input)?;
let (input, context) = parse_single_word(input)?;
Ok((input, GenFsCon::new(fs, path, context)))
}
}
impl<'a> PolicyStatement<'a> {
fn parse(input: &'a str) -> IResult<&'a str, Self> {
let (input, _) = space0(input)?;
let (input, statement) = alt((
map(NormalPerm::parse, PolicyStatement::NormalPerm),
map(XPerm::parse, PolicyStatement::XPerm),
map(TypeState::parse, PolicyStatement::TypeState),
map(Type::parse, PolicyStatement::Type),
map(TypeAttr::parse, PolicyStatement::TypeAttr),
map(Attr::parse, PolicyStatement::Attr),
map(TypeTransition::parse, PolicyStatement::TypeTransition),
map(TypeChange::parse, PolicyStatement::TypeChange),
map(GenFsCon::parse, PolicyStatement::GenFsCon),
))
.parse(input)?;
let (input, _) = space0(input)?;
let (input, _) = take_while(|c| c == ';')(input)?;
let (input, _) = space0(input)?;
Ok((input, statement))
}
}
fn parse_sepolicy<'a, 'b>(input: &'b str, strict: bool) -> Result<Vec<PolicyStatement<'a>>>
where
'b: 'a,
{
let mut statements = vec![];
for line in input.split(['\n', ';']) {
let trimmed_line = line.trim();
if trimmed_line.is_empty() || trimmed_line.starts_with('#') {
continue;
}
if let Ok((_, statement)) = PolicyStatement::parse(trimmed_line) {
statements.push(statement);
} else if strict {
bail!("Failed to parse policy statement: {}", line)
}
}
Ok(statements)
}
const SEPOLICY_MAX_LEN: usize = 128;
const CMD_NORMAL_PERM: u32 = 1;
const CMD_XPERM: u32 = 2;
const CMD_TYPE_STATE: u32 = 3;
const CMD_TYPE: u32 = 4;
const CMD_TYPE_ATTR: u32 = 5;
const CMD_ATTR: u32 = 6;
const CMD_TYPE_TRANSITION: u32 = 7;
const CMD_TYPE_CHANGE: u32 = 8;
const CMD_GENFSCON: u32 = 9;
#[derive(Debug, Default)]
enum PolicyObject {
All, // for "*", stand for all objects, and is NULL in ffi
One([u8; SEPOLICY_MAX_LEN]),
#[default]
None,
}
impl TryFrom<&str> for PolicyObject {
type Error = anyhow::Error;
fn try_from(s: &str) -> Result<Self> {
anyhow::ensure!(s.len() <= SEPOLICY_MAX_LEN, "policy object too long");
if s == "*" {
return Ok(PolicyObject::All);
}
let mut buf = [0u8; SEPOLICY_MAX_LEN];
buf[..s.len()].copy_from_slice(s.as_bytes());
Ok(PolicyObject::One(buf))
}
}
/// atomic statement, such as: allow domain1 domain2:file1 read;
/// normal statement would be expand to atomic statement, for example:
/// allow domain1 domain2:file1 { read write }; would be expand to two atomic statement
/// allow domain1 domain2:file1 read;allow domain1 domain2:file1 write;
#[allow(clippy::too_many_arguments)]
#[derive(Debug, new)]
struct AtomicStatement {
cmd: u32,
subcmd: u32,
sepol1: PolicyObject,
sepol2: PolicyObject,
sepol3: PolicyObject,
sepol4: PolicyObject,
sepol5: PolicyObject,
sepol6: PolicyObject,
sepol7: PolicyObject,
}
impl<'a> TryFrom<&'a NormalPerm<'a>> for Vec<AtomicStatement> {
type Error = anyhow::Error;
fn try_from(perm: &'a NormalPerm<'a>) -> Result<Self> {
let mut result = vec![];
let subcmd = match perm.op {
"allow" => 1,
"deny" => 2,
"auditallow" => 3,
"dontaudit" => 4,
_ => 0,
};
for &s in &perm.source {
for &t in &perm.target {
for &c in &perm.class {
for &p in &perm.perm {
result.push(AtomicStatement {
cmd: CMD_NORMAL_PERM,
subcmd,
sepol1: s.try_into()?,
sepol2: t.try_into()?,
sepol3: c.try_into()?,
sepol4: p.try_into()?,
sepol5: PolicyObject::None,
sepol6: PolicyObject::None,
sepol7: PolicyObject::None,
});
}
}
}
}
Ok(result)
}
}
impl<'a> TryFrom<&'a XPerm<'a>> for Vec<AtomicStatement> {
type Error = anyhow::Error;
fn try_from(perm: &'a XPerm<'a>) -> Result<Self> {
let mut result = vec![];
let subcmd = match perm.op {
"allowxperm" => 1,
"auditallowxperm" => 2,
"dontauditxperm" => 3,
_ => 0,
};
for &s in &perm.source {
for &t in &perm.target {
for &c in &perm.class {
result.push(AtomicStatement {
cmd: CMD_XPERM,
subcmd,
sepol1: s.try_into()?,
sepol2: t.try_into()?,
sepol3: c.try_into()?,
sepol4: perm.operation.try_into()?,
sepol5: perm.perm_set.try_into()?,
sepol6: PolicyObject::None,
sepol7: PolicyObject::None,
});
}
}
}
Ok(result)
}
}
impl<'a> TryFrom<&'a TypeState<'a>> for Vec<AtomicStatement> {
type Error = anyhow::Error;
fn try_from(perm: &'a TypeState<'a>) -> Result<Self> {
let mut result = vec![];
let subcmd = match perm.op {
"permissive" => 1,
"enforcing" => 2,
_ => 0,
};
for &t in &perm.stype {
result.push(AtomicStatement {
cmd: CMD_TYPE_STATE,
subcmd,
sepol1: t.try_into()?,
sepol2: PolicyObject::None,
sepol3: PolicyObject::None,
sepol4: PolicyObject::None,
sepol5: PolicyObject::None,
sepol6: PolicyObject::None,
sepol7: PolicyObject::None,
});
}
Ok(result)
}
}
impl<'a> TryFrom<&'a Type<'a>> for Vec<AtomicStatement> {
type Error = anyhow::Error;
fn try_from(perm: &'a Type<'a>) -> Result<Self> {
let mut result = vec![];
for &attr in &perm.attrs {
result.push(AtomicStatement {
cmd: CMD_TYPE,
subcmd: 0,
sepol1: perm.name.try_into()?,
sepol2: attr.try_into()?,
sepol3: PolicyObject::None,
sepol4: PolicyObject::None,
sepol5: PolicyObject::None,
sepol6: PolicyObject::None,
sepol7: PolicyObject::None,
});
}
Ok(result)
}
}
impl<'a> TryFrom<&'a TypeAttr<'a>> for Vec<AtomicStatement> {
type Error = anyhow::Error;
fn try_from(perm: &'a TypeAttr<'a>) -> Result<Self> {
let mut result = vec![];
for &t in &perm.stype {
for &attr in &perm.sattr {
result.push(AtomicStatement {
cmd: CMD_TYPE_ATTR,
subcmd: 0,
sepol1: t.try_into()?,
sepol2: attr.try_into()?,
sepol3: PolicyObject::None,
sepol4: PolicyObject::None,
sepol5: PolicyObject::None,
sepol6: PolicyObject::None,
sepol7: PolicyObject::None,
});
}
}
Ok(result)
}
}
impl<'a> TryFrom<&'a Attr<'a>> for Vec<AtomicStatement> {
type Error = anyhow::Error;
fn try_from(perm: &'a Attr<'a>) -> Result<Self> {
let result = vec![AtomicStatement {
cmd: CMD_ATTR,
subcmd: 0,
sepol1: perm.name.try_into()?,
sepol2: PolicyObject::None,
sepol3: PolicyObject::None,
sepol4: PolicyObject::None,
sepol5: PolicyObject::None,
sepol6: PolicyObject::None,
sepol7: PolicyObject::None,
}];
Ok(result)
}
}
impl<'a> TryFrom<&'a TypeTransition<'a>> for Vec<AtomicStatement> {
type Error = anyhow::Error;
fn try_from(perm: &'a TypeTransition<'a>) -> Result<Self> {
let mut result = vec![];
let obj = match perm.object_name {
Some(obj) => obj.try_into()?,
None => PolicyObject::None,
};
result.push(AtomicStatement {
cmd: CMD_TYPE_TRANSITION,
subcmd: 0,
sepol1: perm.source.try_into()?,
sepol2: perm.target.try_into()?,
sepol3: perm.class.try_into()?,
sepol4: perm.default_type.try_into()?,
sepol5: obj,
sepol6: PolicyObject::None,
sepol7: PolicyObject::None,
});
Ok(result)
}
}
impl<'a> TryFrom<&'a TypeChange<'a>> for Vec<AtomicStatement> {
type Error = anyhow::Error;
fn try_from(perm: &'a TypeChange<'a>) -> Result<Self> {
let mut result = vec![];
let subcmd = match perm.op {
"type_change" => 1,
"type_member" => 2,
_ => 0,
};
result.push(AtomicStatement {
cmd: CMD_TYPE_CHANGE,
subcmd,
sepol1: perm.source.try_into()?,
sepol2: perm.target.try_into()?,
sepol3: perm.class.try_into()?,
sepol4: perm.default_type.try_into()?,
sepol5: PolicyObject::None,
sepol6: PolicyObject::None,
sepol7: PolicyObject::None,
});
Ok(result)
}
}
impl<'a> TryFrom<&'a GenFsCon<'a>> for Vec<AtomicStatement> {
type Error = anyhow::Error;
fn try_from(perm: &'a GenFsCon<'a>) -> Result<Self> {
let result = vec![AtomicStatement {
cmd: CMD_GENFSCON,
subcmd: 0,
sepol1: perm.fs_name.try_into()?,
sepol2: perm.partial_path.try_into()?,
sepol3: perm.fs_context.try_into()?,
sepol4: PolicyObject::None,
sepol5: PolicyObject::None,
sepol6: PolicyObject::None,
sepol7: PolicyObject::None,
}];
Ok(result)
}
}
impl<'a> TryFrom<&'a PolicyStatement<'a>> for Vec<AtomicStatement> {
type Error = anyhow::Error;
fn try_from(value: &'a PolicyStatement) -> Result<Self> {
match value {
PolicyStatement::NormalPerm(perm) => perm.try_into(),
PolicyStatement::XPerm(perm) => perm.try_into(),
PolicyStatement::TypeState(perm) => perm.try_into(),
PolicyStatement::Type(perm) => perm.try_into(),
PolicyStatement::TypeAttr(perm) => perm.try_into(),
PolicyStatement::Attr(perm) => perm.try_into(),
PolicyStatement::TypeTransition(perm) => perm.try_into(),
PolicyStatement::TypeChange(perm) => perm.try_into(),
PolicyStatement::GenFsCon(perm) => perm.try_into(),
}
}
}
////////////////////////////////////////////////////////////////
/// for C FFI to call kernel interface
///////////////////////////////////////////////////////////////
#[derive(Debug)]
#[repr(C)]
struct FfiPolicy {
cmd: u32,
subcmd: u32,
sepol1: *const ffi::c_char,
sepol2: *const ffi::c_char,
sepol3: *const ffi::c_char,
sepol4: *const ffi::c_char,
sepol5: *const ffi::c_char,
sepol6: *const ffi::c_char,
sepol7: *const ffi::c_char,
}
fn to_c_ptr(pol: &PolicyObject) -> *const ffi::c_char {
match pol {
PolicyObject::None | PolicyObject::All => std::ptr::null(),
PolicyObject::One(s) => s.as_ptr().cast::<ffi::c_char>(),
}
}
impl From<AtomicStatement> for FfiPolicy {
fn from(policy: AtomicStatement) -> FfiPolicy {
FfiPolicy {
cmd: policy.cmd,
subcmd: policy.subcmd,
sepol1: to_c_ptr(&policy.sepol1),
sepol2: to_c_ptr(&policy.sepol2),
sepol3: to_c_ptr(&policy.sepol3),
sepol4: to_c_ptr(&policy.sepol4),
sepol5: to_c_ptr(&policy.sepol5),
sepol6: to_c_ptr(&policy.sepol6),
sepol7: to_c_ptr(&policy.sepol7),
}
}
}
#[cfg(any(target_os = "linux", target_os = "android"))]
fn apply_one_rule<'a>(statement: &'a PolicyStatement<'a>, strict: bool) -> Result<()> {
let policies: Vec<AtomicStatement> = statement.try_into()?;
for policy in policies {
let ffi_policy = FfiPolicy::from(policy);
let cmd = crate::ksucalls::SetSepolicyCmd {
cmd: 0,
arg: &ffi_policy as *const _ as u64,
};
if let Err(e) = crate::ksucalls::set_sepolicy(&cmd) {
log::warn!("apply rule {:?} failed: {}", statement, e);
if strict {
return Err(anyhow::anyhow!("apply rule {:?} failed: {}", statement, e));
}
}
}
Ok(())
}
#[cfg(not(any(target_os = "linux", target_os = "android")))]
fn apply_one_rule<'a>(_statement: &'a PolicyStatement<'a>, _strict: bool) -> Result<()> {
unimplemented!()
}
pub fn live_patch(policy: &str) -> Result<()> {
let result = parse_sepolicy(policy.trim(), false)?;
for statement in result {
println!("{statement:?}");
apply_one_rule(&statement, false)?;
}
Ok(())
}
pub fn apply_file<P: AsRef<Path>>(path: P) -> Result<()> {
let input = std::fs::read_to_string(path)?;
live_patch(&input)
}
pub fn check_rule(policy: &str) -> Result<()> {
let path = Path::new(policy);
let policy = if path.exists() {
std::fs::read_to_string(path)?
} else {
policy.to_string()
};
parse_sepolicy(policy.trim(), true)?;
Ok(())
}

320
userspace/ksud/src/su.rs Normal file
View file

@ -0,0 +1,320 @@
use crate::{
defs,
utils::{self, umask},
};
use anyhow::{Context, Ok, Result, bail};
use getopts::Options;
use libc::c_int;
use log::{error, warn};
#[cfg(unix)]
use std::os::unix::process::CommandExt;
use std::{env, ffi::CStr, path::PathBuf, process::Command};
#[cfg(any(target_os = "linux", target_os = "android"))]
use crate::ksucalls::get_wrapped_fd;
#[cfg(any(target_os = "linux", target_os = "android"))]
use rustix::{
process::getuid,
thread::{Gid, Uid, set_thread_res_gid, set_thread_res_uid},
};
#[cfg(any(target_os = "linux", target_os = "android"))]
pub fn grant_root(global_mnt: bool) -> Result<()> {
crate::ksucalls::grant_root()?;
let mut command = Command::new("sh");
let command = unsafe {
command.pre_exec(move || {
if global_mnt {
let _ = utils::switch_mnt_ns(1);
}
Result::Ok(())
})
};
// add /data/adb/ksu/bin to PATH
#[cfg(any(target_os = "linux", target_os = "android"))]
add_path_to_env(defs::BINARY_DIR)?;
Err(command.exec().into())
}
#[cfg(not(any(target_os = "linux", target_os = "android")))]
pub fn grant_root(_global_mnt: bool) -> Result<()> {
unimplemented!("grant_root is only available on android");
}
fn print_usage(program: &str, opts: Options) {
let brief = format!("KernelSU\n\nUsage: {program} [options] [-] [user [argument...]]");
print!("{}", opts.usage(&brief));
}
fn set_identity(uid: u32, gid: u32, groups: &[u32]) {
#[cfg(any(target_os = "linux", target_os = "android"))]
{
rustix::thread::set_thread_groups(
groups
.iter()
.map(|g| unsafe { Gid::from_raw(*g) })
.collect::<Vec<_>>()
.as_ref(),
)
.ok();
let gid = unsafe { Gid::from_raw(gid) };
let uid = unsafe { Uid::from_raw(uid) };
set_thread_res_gid(gid, gid, gid).ok();
set_thread_res_uid(uid, uid, uid).ok();
}
}
#[cfg(target_os = "android")]
fn wrap_tty(fd: c_int) {
let inner_fn = move || -> Result<()> {
if unsafe { libc::isatty(fd) != 1 } {
warn!("not a tty: {fd}");
return Ok(());
}
let new_fd = get_wrapped_fd(fd).context("get_wrapped_fd")?;
if unsafe { libc::dup2(new_fd, fd) } == -1 {
bail!("dup {new_fd} -> {fd} errno: {}", unsafe {
*libc::__errno()
});
} else {
unsafe { libc::close(new_fd) };
Ok(())
}
};
if let Err(e) = inner_fn() {
error!("wrap tty {fd}: {e:?}");
}
}
#[cfg(not(any(target_os = "linux", target_os = "android")))]
pub fn root_shell() -> Result<()> {
unimplemented!()
}
#[cfg(any(target_os = "linux", target_os = "android"))]
pub fn root_shell() -> Result<()> {
// we are root now, this was set in kernel!
use anyhow::anyhow;
let env_args: Vec<String> = env::args().collect();
let program = env_args[0].clone();
let args = env_args
.iter()
.position(|arg| arg == "-c")
.map(|i| {
let rest = env_args[i + 1..].to_vec();
let mut new_args = env_args[..i].to_vec();
new_args.push("-c".to_string());
if !rest.is_empty() {
new_args.push(rest.join(" "));
}
new_args
})
.unwrap_or_else(|| env_args.clone());
let mut opts = Options::new();
opts.optopt(
"c",
"command",
"pass COMMAND to the invoked shell",
"COMMAND",
);
opts.optflag("h", "help", "display this help message and exit");
opts.optflag("l", "login", "pretend the shell to be a login shell");
opts.optflag(
"p",
"preserve-environment",
"preserve the entire environment",
);
opts.optopt(
"s",
"shell",
"use SHELL instead of the default /system/bin/sh",
"SHELL",
);
opts.optflag("v", "version", "display version number and exit");
opts.optflag("V", "", "display version code and exit");
opts.optflag(
"M",
"mount-master",
"force run in the global mount namespace",
);
opts.optopt("g", "group", "Specify the primary group", "GROUP");
opts.optmulti(
"G",
"supp-group",
"Specify a supplementary group. The first specified supplementary group is also used as a primary group if the option -g is not specified.",
"GROUP",
);
opts.optflag("W", "no-wrapper", "don't use ksu fd wrapper");
// Replace -cn with -z, -mm with -M for supporting getopt_long
let args = args
.into_iter()
.map(|e| {
if e == "-mm" {
"-M".to_string()
} else if e == "-cn" {
"-z".to_string()
} else {
e
}
})
.collect::<Vec<String>>();
let matches = match opts.parse(&args[1..]) {
Result::Ok(m) => m,
Err(f) => {
println!("{f}");
print_usage(&program, opts);
std::process::exit(-1);
}
};
if matches.opt_present("h") {
print_usage(&program, opts);
return Ok(());
}
if matches.opt_present("v") {
println!("{}:KernelSU", defs::VERSION_NAME);
return Ok(());
}
if matches.opt_present("V") {
println!("{}", defs::VERSION_CODE);
return Ok(());
}
let shell = matches.opt_str("s").unwrap_or("/system/bin/sh".to_string());
let mut is_login = matches.opt_present("l");
let preserve_env = matches.opt_present("p");
let mount_master = matches.opt_present("M");
let use_fd_wrapper = !matches.opt_present("W");
let groups = matches
.opt_strs("G")
.into_iter()
.map(|g| g.parse::<u32>().map_err(|_| anyhow!("Invalid GID: {}", g)))
.collect::<Result<Vec<_>, _>>()?;
// if -g provided, use it.
let mut gid = matches
.opt_str("g")
.map(|g| g.parse::<u32>().map_err(|_| anyhow!("Invalid GID: {}", g)))
.transpose()?;
// otherwise, use the first gid of groups.
if gid.is_none() && !groups.is_empty() {
gid = Some(groups[0]);
}
// we've make sure that -c is the last option and it already contains the whole command, no need to construct it again
let args = matches
.opt_str("c")
.map(|cmd| vec!["-c".to_string(), cmd])
.unwrap_or_default();
let mut free_idx = 0;
if !matches.free.is_empty() && matches.free[free_idx] == "-" {
is_login = true;
free_idx += 1;
}
// use current uid if no user specified, these has been done in kernel!
let mut uid = getuid().as_raw();
if free_idx < matches.free.len() {
let name = &matches.free[free_idx];
uid = unsafe {
#[cfg(any(target_arch = "aarch64", target_arch = "arm"))]
let pw = libc::getpwnam(name.as_ptr()).as_ref();
#[cfg(target_arch = "x86_64")]
let pw = libc::getpwnam(name.as_ptr() as *const i8).as_ref();
match pw {
Some(pw) => pw.pw_uid,
None => name.parse::<u32>().unwrap_or(0),
}
}
}
// if there is no gid provided, use uid.
let gid = gid.unwrap_or(uid);
// https://github.com/topjohnwu/Magisk/blob/master/native/src/su/su_daemon.cpp#L408
let arg0 = if is_login { "-" } else { &shell };
let mut command = &mut Command::new(&shell);
if !preserve_env {
// This is actually incorrect, i don't know why.
// command = command.env_clear();
let pw = unsafe { libc::getpwuid(uid).as_ref() };
if let Some(pw) = pw {
let home = unsafe { CStr::from_ptr(pw.pw_dir) };
let pw_name = unsafe { CStr::from_ptr(pw.pw_name) };
let home = home.to_string_lossy();
let pw_name = pw_name.to_string_lossy();
command = command
.env("HOME", home.as_ref())
.env("USER", pw_name.as_ref())
.env("LOGNAME", pw_name.as_ref())
.env("SHELL", &shell);
}
}
// add /data/adb/ksu/bin to PATH
#[cfg(any(target_os = "linux", target_os = "android"))]
add_path_to_env(defs::BINARY_DIR)?;
// when KSURC_PATH exists and ENV is not set, set ENV to KSURC_PATH
if PathBuf::from(defs::KSURC_PATH).exists() && env::var("ENV").is_err() {
command = command.env("ENV", defs::KSURC_PATH);
}
// escape from the current cgroup and become session leader
// WARNING!!! This cause some root shell hang forever!
// command = command.process_group(0);
command = unsafe {
command.pre_exec(move || {
umask(0o22);
utils::switch_cgroups();
// switch to global mount namespace
#[cfg(any(target_os = "linux", target_os = "android"))]
if mount_master {
let _ = utils::switch_mnt_ns(1);
}
#[cfg(target_os = "android")]
if use_fd_wrapper {
wrap_tty(0);
wrap_tty(1);
wrap_tty(2);
}
set_identity(uid, gid, &groups);
Result::Ok(())
})
};
command = command.args(args).arg0(arg0);
Err(command.exec().into())
}
fn add_path_to_env(path: &str) -> Result<()> {
let mut paths =
env::var_os("PATH").map_or(Vec::new(), |val| env::split_paths(&val).collect::<Vec<_>>());
let new_path = PathBuf::from(path.trim_end_matches('/'));
paths.push(new_path);
let new_path_env = env::join_paths(paths)?;
unsafe { env::set_var("PATH", new_path_env) };
Ok(())
}

View file

@ -0,0 +1,88 @@
use std::{
fs,
io::Write,
os::unix::{
fs::{PermissionsExt, symlink},
process::CommandExt,
},
process::{Command, Stdio},
};
use anyhow::Result;
use log::{info, warn};
const SCANNER_PATH: &str = "/data/adb/uid_scanner";
const LINK_DIR: &str = "/data/adb/ksu/bin";
const LINK_PATH: &str = "/data/adb/ksu/bin/uid_scanner";
const SERVICE_DIR: &str = "/data/adb/service.d";
const SERVICE_PATH: &str = "/data/adb/service.d/uid_scanner.sh";
pub fn start_uid_scanner_daemon() -> Result<()> {
if !fs::exists(SCANNER_PATH)? {
warn!("uid scanner binary not found at {}", SCANNER_PATH);
return Ok(());
}
if let Err(e) = fs::set_permissions(SCANNER_PATH, fs::Permissions::from_mode(0o755)) {
warn!("failed to set permissions for {}: {}", SCANNER_PATH, e);
}
#[cfg(unix)]
{
if let Err(e) = fs::create_dir_all(LINK_DIR) {
warn!("failed to create {}: {}", LINK_DIR, e);
} else if !fs::exists(LINK_PATH)? {
match symlink(SCANNER_PATH, LINK_PATH) {
Ok(_) => info!("created symlink {} -> {}", SCANNER_PATH, LINK_PATH),
Err(e) => warn!("failed to create symlink: {}", e),
}
}
}
if let Err(e) = fs::create_dir_all(SERVICE_DIR) {
warn!("failed to create {}: {}", SERVICE_DIR, e);
}
if !fs::exists(SERVICE_PATH)? {
let content = include_str!("uid_scanner.sh");
match fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(SERVICE_PATH)
.and_then(|mut f| {
f.write_all(content.as_bytes())?;
f.sync_all()?;
fs::set_permissions(SERVICE_PATH, fs::Permissions::from_mode(0o755))
}) {
Ok(_) => info!("created service script {}", SERVICE_PATH),
Err(e) => warn!("failed to write {}: {}", SERVICE_PATH, e),
}
}
info!("starting uid scanner daemon with highest priority");
let mut cmd = Command::new(SCANNER_PATH);
cmd.arg("start")
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.current_dir("/");
unsafe {
cmd.pre_exec(|| {
libc::nice(-20);
libc::setsid();
Ok(())
});
}
match cmd.spawn() {
Ok(child) => {
info!("uid scanner daemon started with pid: {}", child.id());
std::mem::drop(child);
}
Err(e) => warn!("failed to start uid scanner daemon: {}", e),
}
Ok(())
}

View file

@ -0,0 +1,5 @@
#!/system/bin/sh
# KSU uid_scanner auto-restart script
until [ -d "/sdcard/Android" ]; do sleep 1; done
sleep 10
/data/adb/uid_scanner restart

View file

@ -0,0 +1,264 @@
use anyhow::{Context, Result, anyhow};
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::{Path, PathBuf};
use crate::ksucalls::ksuctl;
const MAGIC_NUMBER_HEADER: &[u8; 4] = b"KUMT";
const MAGIC_VERSION: u32 = 1;
const CONFIG_FILE: &str = "/data/adb/ksu/.umount";
const KSU_IOCTL_UMOUNT_MANAGER: u32 = 0xc0004b6b; // _IOC(_IOC_READ|_IOC_WRITE, 'K', 107, 0)
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct UmountEntry {
pub path: String,
pub flags: i32,
pub is_default: bool,
}
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct UmountConfig {
pub entries: Vec<UmountEntry>,
}
pub struct UmountManager {
config: UmountConfig,
config_path: PathBuf,
}
#[repr(C)]
#[derive(Clone, Copy)]
struct UmountManagerCmd {
pub operation: u32,
pub path: [u8; 256],
pub flags: i32,
pub count: u32,
pub entries_ptr: u64,
}
impl Default for UmountManagerCmd {
fn default() -> Self {
UmountManagerCmd {
operation: 0,
path: [0; 256],
flags: 0,
count: 0,
entries_ptr: 0,
}
}
}
impl UmountManager {
pub fn new(config_path: Option<PathBuf>) -> Result<Self> {
let path = config_path.unwrap_or_else(|| PathBuf::from(CONFIG_FILE));
let config = if path.exists() {
Self::load_config(&path)?
} else {
UmountConfig {
entries: Vec::new(),
}
};
Ok(UmountManager {
config,
config_path: path,
})
}
fn load_config(path: &Path) -> Result<UmountConfig> {
let data = fs::read(path).context("Failed to read config file")?;
if data.len() < 8 {
return Err(anyhow!("Invalid config file: too small"));
}
let header = &data[0..4];
if header != MAGIC_NUMBER_HEADER {
return Err(anyhow!("Invalid config file: wrong magic number"));
}
let version = u32::from_le_bytes([data[4], data[5], data[6], data[7]]);
if version != MAGIC_VERSION {
return Err(anyhow!("Unsupported config version: {}", version));
}
let json_data = &data[8..];
let config: UmountConfig =
serde_json::from_slice(json_data).context("Failed to parse config JSON")?;
Ok(config)
}
pub fn save_config(&self) -> Result<()> {
let dir = self.config_path.parent().unwrap();
fs::create_dir_all(dir).context("Failed to create config directory")?;
let mut data = Vec::new();
data.extend_from_slice(MAGIC_NUMBER_HEADER);
data.extend_from_slice(&MAGIC_VERSION.to_le_bytes());
let json = serde_json::to_vec(&self.config).context("Failed to serialize config")?;
data.extend_from_slice(&json);
fs::write(&self.config_path, &data).context("Failed to write config file")?;
Ok(())
}
pub fn add_entry(&mut self, path: &str, flags: i32) -> Result<()> {
let exists = self.config.entries.iter().any(|e| e.path == path);
if exists {
return Err(anyhow!("Entry already exists: {}", path));
}
let entry = UmountEntry {
path: path.to_string(),
flags,
is_default: false,
};
self.config.entries.push(entry);
Ok(())
}
pub fn remove_entry(&mut self, path: &str) -> Result<()> {
let before = self.config.entries.len();
self.config.entries.retain(|e| e.path != path);
if before == self.config.entries.len() {
return Err(anyhow!("Entry not found: {}", path));
}
Ok(())
}
pub fn list_entries(&self) -> Vec<UmountEntry> {
self.config.entries.clone()
}
pub fn clear_custom_entries(&mut self) -> Result<()> {
self.config.entries.retain(|e| e.is_default);
Ok(())
}
pub fn apply_to_kernel(&self) -> Result<()> {
for entry in &self.config.entries {
Self::kernel_add_entry(entry)?;
}
Ok(())
}
fn kernel_add_entry(entry: &UmountEntry) -> Result<()> {
let mut cmd = UmountManagerCmd {
operation: 0,
flags: entry.flags,
..Default::default()
};
let path_bytes = entry.path.as_bytes();
if path_bytes.len() >= cmd.path.len() {
return Err(anyhow!("Path too long: {}", entry.path));
}
cmd.path[..path_bytes.len()].copy_from_slice(path_bytes);
umount_manager_ioctl(&cmd).context(format!("Failed to add entry: {}", entry.path))?;
Ok(())
}
}
pub fn init_umount_manager() -> Result<UmountManager> {
let manager = UmountManager::new(None)?;
if !Path::new(CONFIG_FILE).exists() {
manager.save_config()?;
}
Ok(manager)
}
pub fn add_umount_path(path: &str, flags: i32) -> Result<()> {
let mut manager = init_umount_manager()?;
manager.add_entry(path, flags)?;
manager.save_config()?;
println!("✓ Added umount path: {}", path);
Ok(())
}
pub fn remove_umount_path(path: &str) -> Result<()> {
let mut manager = init_umount_manager()?;
manager.remove_entry(path)?;
manager.save_config()?;
println!("✓ Removed umount path: {}", path);
Ok(())
}
pub fn list_umount_paths() -> Result<()> {
let manager = init_umount_manager()?;
let entries = manager.list_entries();
if entries.is_empty() {
println!("No umount paths configured");
return Ok(());
}
println!("{:<30} {:<8} {:<10}", "Path", "Flags", "Default");
println!("{}", "=".repeat(60));
for entry in entries {
println!(
"{:<30} {:<8} {:<10}",
entry.path,
entry.flags,
if entry.is_default { "Yes" } else { "No" }
);
}
Ok(())
}
#[cfg(any(target_os = "linux", target_os = "android"))]
fn umount_manager_ioctl(cmd: &UmountManagerCmd) -> std::io::Result<()> {
let mut ioctl_cmd = *cmd;
ksuctl(KSU_IOCTL_UMOUNT_MANAGER, &mut ioctl_cmd as *mut _)?;
Ok(())
}
#[cfg(not(any(target_os = "linux", target_os = "android")))]
pub fn umount_manager_ioctl(_cmd: &UmountManagerCmd) -> std::io::Result<()> {
Err(std::io::Error::from_raw_os_error(libc::ENOSYS))
}
pub fn clear_custom_paths() -> Result<()> {
let mut manager = init_umount_manager()?;
manager.clear_custom_entries()?;
manager.save_config()?;
println!("✓ Cleared all custom paths");
Ok(())
}
pub fn save_umount_config() -> Result<()> {
let manager = init_umount_manager()?;
manager.save_config()?;
println!("✓ Configuration saved to: {}", CONFIG_FILE);
Ok(())
}
pub fn load_and_apply_config() -> Result<()> {
let manager = init_umount_manager()?;
manager.apply_to_kernel()?;
println!("✓ Configuration applied to kernel");
Ok(())
}
pub fn apply_config_to_kernel() -> Result<()> {
let manager = init_umount_manager()?;
manager.apply_to_kernel()?;
println!(
"✓ Applied {} entries to kernel",
manager.list_entries().len()
);
Ok(())
}

229
userspace/ksud/src/utils.rs Normal file
View file

@ -0,0 +1,229 @@
use anyhow::{Context, Error, Ok, Result, bail};
use std::{
fs::{File, OpenOptions, create_dir_all, remove_file, write},
io::{
ErrorKind::{AlreadyExists, NotFound},
Write,
},
path::Path,
process::Command,
};
use crate::{assets, boot_patch, defs, ksucalls, module, restorecon};
#[allow(unused_imports)]
use std::fs::{Permissions, set_permissions};
#[cfg(unix)]
use std::os::unix::prelude::PermissionsExt;
use std::path::PathBuf;
#[cfg(any(target_os = "linux", target_os = "android"))]
use rustix::{
process,
thread::{LinkNameSpaceType, move_into_link_name_space},
};
pub fn ensure_clean_dir(dir: impl AsRef<Path>) -> Result<()> {
let path = dir.as_ref();
log::debug!("ensure_clean_dir: {}", path.display());
if path.exists() {
log::debug!("ensure_clean_dir: {} exists, remove it", path.display());
std::fs::remove_dir_all(path)?;
}
Ok(std::fs::create_dir_all(path)?)
}
pub fn ensure_file_exists<T: AsRef<Path>>(file: T) -> Result<()> {
match File::options().write(true).create_new(true).open(&file) {
std::result::Result::Ok(_) => Ok(()),
Err(err) => {
if err.kind() == AlreadyExists && file.as_ref().is_file() {
Ok(())
} else {
Err(Error::from(err))
.with_context(|| format!("{} is not a regular file", file.as_ref().display()))
}
}
}
}
pub fn ensure_dir_exists<T: AsRef<Path>>(dir: T) -> Result<()> {
let result = create_dir_all(&dir);
if dir.as_ref().is_dir() && result.is_ok() {
Ok(())
} else {
bail!("{} is not a regular directory", dir.as_ref().display())
}
}
pub fn ensure_binary<T: AsRef<Path>>(
path: T,
contents: &[u8],
ignore_if_exist: bool,
) -> Result<()> {
if ignore_if_exist && path.as_ref().exists() {
return Ok(());
}
ensure_dir_exists(path.as_ref().parent().ok_or_else(|| {
anyhow::anyhow!(
"{} does not have parent directory",
path.as_ref().to_string_lossy()
)
})?)?;
if let Err(e) = remove_file(path.as_ref())
&& e.kind() != NotFound
{
return Err(Error::from(e))
.with_context(|| format!("failed to unlink {}", path.as_ref().display()));
}
write(&path, contents)?;
#[cfg(unix)]
set_permissions(&path, Permissions::from_mode(0o755))?;
Ok(())
}
#[cfg(any(target_os = "linux", target_os = "android"))]
pub fn getprop(prop: &str) -> Option<String> {
android_properties::getprop(prop).value()
}
#[cfg(not(any(target_os = "linux", target_os = "android")))]
pub fn getprop(_prop: &str) -> Option<String> {
unimplemented!()
}
pub fn is_safe_mode() -> bool {
let safemode = getprop("persist.sys.safemode")
.filter(|prop| prop == "1")
.is_some()
|| getprop("ro.sys.safemode")
.filter(|prop| prop == "1")
.is_some();
log::info!("safemode: {safemode}");
if safemode {
return true;
}
let safemode = ksucalls::check_kernel_safemode();
log::info!("kernel_safemode: {safemode}");
safemode
}
pub fn get_zip_uncompressed_size(zip_path: &str) -> Result<u64> {
let mut zip = zip::ZipArchive::new(std::fs::File::open(zip_path)?)?;
let total: u64 = (0..zip.len())
.map(|i| zip.by_index(i).unwrap().size())
.sum();
Ok(total)
}
#[cfg(any(target_os = "linux", target_os = "android"))]
pub fn switch_mnt_ns(pid: i32) -> Result<()> {
use rustix::{
fd::AsFd,
fs::{Mode, OFlags, open},
};
let path = format!("/proc/{pid}/ns/mnt");
let fd = open(path, OFlags::RDONLY, Mode::from_raw_mode(0))?;
let current_dir = std::env::current_dir();
move_into_link_name_space(fd.as_fd(), Some(LinkNameSpaceType::Mount))?;
if let std::result::Result::Ok(current_dir) = current_dir {
let _ = std::env::set_current_dir(current_dir);
}
Ok(())
}
fn switch_cgroup(grp: &str, pid: u32) {
let path = Path::new(grp).join("cgroup.procs");
if !path.exists() {
return;
}
let fp = OpenOptions::new().append(true).open(path);
if let std::result::Result::Ok(mut fp) = fp {
let _ = write!(fp, "{pid}");
}
}
pub fn switch_cgroups() {
let pid = std::process::id();
switch_cgroup("/acct", pid);
switch_cgroup("/dev/cg2_bpf", pid);
switch_cgroup("/sys/fs/cgroup", pid);
if getprop("ro.config.per_app_memcg")
.filter(|prop| prop == "false")
.is_none()
{
switch_cgroup("/dev/memcg/apps", pid);
}
}
#[cfg(any(target_os = "linux", target_os = "android"))]
pub fn umask(mask: u32) {
process::umask(rustix::fs::Mode::from_raw_mode(mask));
}
#[cfg(not(any(target_os = "linux", target_os = "android")))]
pub fn umask(_mask: u32) {
unimplemented!("umask is not supported on this platform")
}
pub fn has_magisk() -> bool {
which::which("magisk").is_ok()
}
#[cfg(target_os = "android")]
fn link_ksud_to_bin() -> Result<()> {
let ksu_bin = PathBuf::from(defs::DAEMON_PATH);
let ksu_bin_link = PathBuf::from(defs::DAEMON_LINK_PATH);
if ksu_bin.exists() && !ksu_bin_link.exists() {
std::os::unix::fs::symlink(&ksu_bin, &ksu_bin_link)?;
}
Ok(())
}
pub fn install(magiskboot: Option<PathBuf>) -> Result<()> {
ensure_dir_exists(defs::ADB_DIR)?;
std::fs::copy(
std::env::current_exe().with_context(|| "Failed to get self exe path")?,
defs::DAEMON_PATH,
)?;
restorecon::lsetfilecon(defs::DAEMON_PATH, restorecon::ADB_CON)?;
// install binary assets
assets::ensure_binaries(false).with_context(|| "Failed to extract assets")?;
#[cfg(target_os = "android")]
link_ksud_to_bin()?;
if let Some(magiskboot) = magiskboot {
ensure_dir_exists(defs::BINARY_DIR)?;
let _ = std::fs::copy(magiskboot, defs::MAGISKBOOT_PATH);
}
Ok(())
}
pub fn uninstall(magiskboot_path: Option<PathBuf>) -> Result<()> {
if Path::new(defs::MODULE_DIR).exists() {
println!("- Uninstall modules..");
module::uninstall_all_modules()?;
module::prune_modules()?;
}
println!("- Removing directories..");
std::fs::remove_dir_all(defs::WORKING_DIR).ok();
std::fs::remove_file(defs::DAEMON_PATH).ok();
std::fs::remove_dir_all(defs::MODULE_DIR).ok();
println!("- Restore boot image..");
boot_patch::restore(None, magiskboot_path, true)?;
println!("- Uninstall KernelSU manager..");
Command::new("pm")
.args(["uninstall", "com.sukisu.ultra"])
.spawn()?;
println!("- Rebooting in 5 seconds..");
std::thread::sleep(std::time::Duration::from_secs(5));
Command::new("reboot").spawn()?;
Ok(())
}