Files
SukiSU-Ultra/userspace/ksud/src/module.rs

817 lines
26 KiB
Rust

#[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 regex_lite::Regex;
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<()> {
let re = Regex::new(r"^[a-zA-Z][a-zA-Z0-9._-]+$")?;
if re.is_match(module_id) {
Ok(())
} else {
Err(anyhow!(
"Invalid module ID: '{}'. Must match /^[a-zA-Z][a-zA-Z0-9._-]+$/",
module_id
))
}
}
/// 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)
}