From 4769065cfc6e258b0d82d043ce67b6169d10c6fc Mon Sep 17 00:00:00 2001 From: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com> Date: Fri, 7 Nov 2025 13:15:07 +0800 Subject: [PATCH] ksud: Implementing editable, removable mount points --- userspace/ksud/Cargo.toml | 1 + userspace/ksud/src/cli.rs | 57 +++++ userspace/ksud/src/init_event.rs | 5 + userspace/ksud/src/ksucalls.rs | 42 ++++ userspace/ksud/src/main.rs | 1 + userspace/ksud/src/umount_manager.rs | 297 +++++++++++++++++++++++++++ 6 files changed, 403 insertions(+) create mode 100644 userspace/ksud/src/umount_manager.rs diff --git a/userspace/ksud/Cargo.toml b/userspace/ksud/Cargo.toml index a7acbc7f..625effc8 100644 --- a/userspace/ksud/Cargo.toml +++ b/userspace/ksud/Cargo.toml @@ -46,6 +46,7 @@ tempfile = "3" chrono = "0.4" regex-lite = "0.1" fs4 = "0.13" +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", branch = "main", features = [ diff --git a/userspace/ksud/src/cli.rs b/userspace/ksud/src/cli.rs index 64ea5a3e..ce67a6cb 100644 --- a/userspace/ksud/src/cli.rs +++ b/userspace/ksud/src/cli.rs @@ -137,6 +137,12 @@ enum Commands { command: kpm_cmd::Kpm, }, + /// Manage kernel umount paths + Umount { + #[command(subcommand)] + command: Umount, + }, + /// For developers Debug { #[command(subcommand)] @@ -343,6 +349,44 @@ mod kpm_cmd { } } +#[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")] + check_mnt: bool, + + /// Umount flags (0 or 8 for MNT_DETACH) + #[arg(long, default_value = "0")] + 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( @@ -480,6 +524,19 @@ pub fn run() -> Result<()> { Kpm::Version => crate::kpm::kpm_version_loader(), } } + Commands::Umount { command } => match command { + Umount::Add { + path, + check_mnt, + flags, + } => crate::umount_manager::add_umount_path(&path, check_mnt, 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 { diff --git a/userspace/ksud/src/init_event.rs b/userspace/ksud/src/init_event.rs index 315ba1f6..c0f905b5 100644 --- a/userspace/ksud/src/init_event.rs +++ b/userspace/ksud/src/init_event.rs @@ -55,6 +55,11 @@ pub fn on_post_data_fs() -> Result<()> { // Start UID scanner daemon with highest priority 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}"); + } // tell kernel that we've mount the module, so that it can do some optimization ksucalls::report_module_mounted(); diff --git a/userspace/ksud/src/ksucalls.rs b/userspace/ksud/src/ksucalls.rs index acabf09a..48e908f0 100644 --- a/userspace/ksud/src/ksucalls.rs +++ b/userspace/ksud/src/ksucalls.rs @@ -18,6 +18,8 @@ const KSU_IOCTL_SET_FEATURE: u32 = 0x40004b0e; // _IOC(_IOC_WRITE, 'K', 14, 0) const KSU_IOCTL_GET_WRAPPER_FD: u32 = 0x00006f10; // _IOC(_IOC_NONE, 'K', 10000, 0) #[allow(dead_code)] const KSU_IOCTL_KPM: u32 = 0xc0004bc8; // _IOC(_IOC_READ|_IOC_WRITE, 'K', 200, 0) +#[allow(dead_code)] +const KSU_IOCTL_UMOUNT_MANAGER: u32 = 0xc0004b6b; // _IOC(_IOC_READ|_IOC_WRITE, 'K', 107, 0) #[repr(C)] #[derive(Clone, Copy, Default)] @@ -252,3 +254,43 @@ pub fn kpm_ioctl(cmd: &mut KsuKpmCmd) -> std::io::Result<()> { ksuctl(KSU_IOCTL_KPM, cmd as *mut _)?; Ok(()) } + +#[repr(C)] +#[derive(Clone, Copy)] +#[allow(dead_code)] +pub struct UmountManagerCmd { + pub operation: u32, + pub path: [u8; 256], + pub check_mnt: u8, + pub flags: i32, + pub count: u32, + pub entries_ptr: u64, +} + +#[allow(dead_code)] +impl Default for UmountManagerCmd { + fn default() -> Self { + UmountManagerCmd { + operation: 0, + path: [0; 256], + check_mnt: 0, + flags: 0, + count: 0, + entries_ptr: 0, + } + } +} + +#[cfg(any(target_os = "linux", target_os = "android"))] +#[allow(dead_code)] +pub 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")))] +#[allow(dead_code)] +pub fn umount_manager_ioctl(_cmd: &UmountManagerCmd) -> std::io::Result<()> { + Err(std::io::Error::from_raw_os_error(libc::ENOSYS)) +} diff --git a/userspace/ksud/src/main.rs b/userspace/ksud/src/main.rs index 3ff3af02..b6f32c1e 100644 --- a/userspace/ksud/src/main.rs +++ b/userspace/ksud/src/main.rs @@ -17,6 +17,7 @@ mod restorecon; mod sepolicy; mod su; mod uid_scanner; +mod umount_manager; mod utils; fn main() -> anyhow::Result<()> { diff --git a/userspace/ksud/src/umount_manager.rs b/userspace/ksud/src/umount_manager.rs new file mode 100644 index 00000000..e30a76cf --- /dev/null +++ b/userspace/ksud/src/umount_manager.rs @@ -0,0 +1,297 @@ +use crate::ksucalls::UmountManagerCmd; +use anyhow::{Context, Result, anyhow}; +use serde::{Deserialize, Serialize}; +use std::fs; +use std::path::{Path, PathBuf}; + +const MAGIC_NUMBER_HEADER: &[u8; 4] = b"KUMT"; +const MAGIC_VERSION: u32 = 1; +const CONFIG_FILE: &str = "/data/adb/ksu/.umount"; + +#[derive(Clone, Serialize, Deserialize, Debug)] +pub struct UmountEntry { + pub path: String, + pub check_mnt: bool, + pub flags: i32, + pub is_default: bool, +} + +#[derive(Clone, Serialize, Deserialize, Debug)] +pub struct UmountConfig { + pub entries: Vec, +} + +pub struct UmountManager { + config: UmountConfig, + config_path: PathBuf, + defaults: Vec, +} + +impl UmountManager { + pub fn new(config_path: Option) -> Result { + 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, + defaults: Vec::new(), + }) + } + + fn load_config(path: &Path) -> Result { + 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, check_mnt: bool, flags: i32) -> Result<()> { + let exists = self.defaults.iter().chain(&self.config.entries).any(|e| e.path == path); + if exists { + return Err(anyhow!("Entry already exists: {}", path)); + } + + let is_default = Self::get_default_paths().iter().any(|e| e.path == path); + + let entry = UmountEntry { + path: path.to_string(), + check_mnt, + flags, + is_default, + }; + + self.config.entries.push(entry); + Ok(()) + } + + pub fn remove_entry(&mut self, path: &str) -> Result<()> { + let entry = self.config.entries.iter().find(|e| e.path == path); + + if let Some(entry) = entry { + if entry.is_default { + return Err(anyhow!("Cannot remove default entry: {}", path)); + } + } else { + return Err(anyhow!("Entry not found: {}", path)); + } + + self.config.entries.retain(|e| e.path != path); + Ok(()) + } + + pub fn list_entries(&self) -> Vec { + let mut all = self.defaults.clone(); + all.extend(self.config.entries.iter().cloned()); + all + } + + pub fn clear_custom_entries(&mut self) -> Result<()> { + self.config.entries.retain(|e| e.is_default); + Ok(()) + } + + pub fn get_default_paths() -> Vec { + vec![ + UmountEntry { + path: "/odm".to_string(), + check_mnt: true, + flags: 0, + is_default: true, + }, + UmountEntry { + path: "/system".to_string(), + check_mnt: true, + flags: 0, + is_default: true, + }, + UmountEntry { + path: "/vendor".to_string(), + check_mnt: true, + flags: 0, + is_default: true, + }, + UmountEntry { + path: "/product".to_string(), + check_mnt: true, + flags: 0, + is_default: true, + }, + UmountEntry { + path: "/system_ext".to_string(), + check_mnt: true, + flags: 0, + is_default: true, + }, + UmountEntry { + path: "/data/adb/modules".to_string(), + check_mnt: false, + flags: 0x00000002, // MNT_DETACH + is_default: true, + }, + ] + } + + pub fn init_defaults(&mut self) -> Result<()> { + self.defaults = Self::get_default_paths(); + Ok(()) + } + + pub fn apply_to_kernel(&self) -> Result<()> { + for entry in &self.defaults { + let _ = Self::kernel_add_entry(entry); + } + 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, + check_mnt: entry.check_mnt as u8, + 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); + + crate::ksucalls::umount_manager_ioctl(&cmd) + .context(format!("Failed to add entry: {}", entry.path))?; + + Ok(()) + } +} + +pub fn init_umount_manager() -> Result { + let mut manager = UmountManager::new(None)?; + manager.init_defaults()?; + + if !Path::new(CONFIG_FILE).exists() { + manager.save_config()?; + } + + Ok(manager) +} + +pub fn add_umount_path(path: &str, check_mnt: bool, flags: i32) -> Result<()> { + let mut manager = init_umount_manager()?; + manager.add_entry(path, check_mnt, 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} {:<12} {:<8} {:<10}", + "Path", "CheckMnt", "Flags", "Default" + ); + println!("{}", "=".repeat(60)); + + for entry in entries { + println!( + "{:<30} {:<12} {:<8} {:<10}", + entry.path, + entry.check_mnt, + entry.flags, + if entry.is_default { "Yes" } else { "No" } + ); + } + + Ok(()) +} + +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(()) +}