Files
SukiSU-Ultra/userspace/ksud/src/cli.rs
backslashxx 029ae8d389 kernel: expose umount list to ioctl interface (#2950)
This idea is borrowed from simonpunk's susfs4ksu.
What we see here is that, yeah well, lets just have userspace send us
what it
wants unmounted, this is better than hardcoding everything.

This also solves that issue where MNT_DETACH fails, as long as we send
unmountables in proper order.

A small anti-duplicate mechanism is also added.

While in-kernel umount is a bit worse than zygisk-provider-based ones,
this can still
serve as a healthy alternative.

---------

- Remove duplicate checks

Signed-off-by: backslashxx <118538522+backslashxx@users.noreply.github.com>
Co-authored-by: weishu <twsxtd@gmail.com>
Co-authored-by: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com>
2025-11-18 12:36:42 +08:00

686 lines
19 KiB
Rust

use anyhow::{Ok, Result};
use clap::Parser;
use std::path::{Path, PathBuf};
#[cfg(target_os = "android")]
use android_logger::Config;
#[cfg(target_os = "android")]
use log::LevelFilter;
use crate::{
apk_sign, assets, debug, defs, defs::KSUD_VERBOSE_LOG_FILE, 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,
#[arg(short, long, default_value_t = cfg!(debug_assertions))]
verbose: bool,
}
#[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("me.weishu.kernelsu"))]
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,
Mount,
/// 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,
},
/// Uninstall module <id>
Uninstall {
/// module id
id: String,
},
/// Restore module <id>
Restore {
/// 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,
}
#[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 custom paths (keep defaults)
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();
if !cli.verbose && !Path::new(KSUD_VERBOSE_LOG_FILE).exists() {
log::set_max_level(LevelFilter::Info);
}
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::Uninstall { id } => module::uninstall_module(&id),
Module::Restore { id } => module::restore_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(),
}
}
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::Mount => init_event::mount_modules_systemlessly(),
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 {
for c in e.chain() {
log::error!("{c:#?}");
}
log::error!("{:#?}", e.backtrace());
}
result
}