diff --git a/userspace/ksud/src/cli.rs b/userspace/ksud/src/cli.rs index 67229d51..8cdb4a5b 100644 --- a/userspace/ksud/src/cli.rs +++ b/userspace/ksud/src/cli.rs @@ -1,12 +1,13 @@ use anyhow::{Ok, Result}; use clap::Parser; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; #[cfg(target_os = "android")] use android_logger::Config; #[cfg(target_os = "android")] use log::LevelFilter; +use crate::defs::KSUD_VERBOSE_LOG_FILE; use crate::{apk_sign, assets, debug, defs, init_event, ksucalls, module, utils}; /// KernelSU userspace cli @@ -15,6 +16,9 @@ use crate::{apk_sign, assets, debug, defs, init_event, ksucalls, module, utils}; struct Args { #[command(subcommand)] command: Commands, + + #[arg(short, long, default_value_t = cfg!(debug_assertions))] + verbose: bool, } #[derive(clap::Subcommand, Debug)] @@ -161,17 +165,6 @@ enum Debug { Mount, - /// Copy sparse file - Xcp { - /// source file - src: String, - /// destination file - dst: String, - /// punch hole - #[arg(short, long, default_value = "false")] - punch_hole: bool, - }, - /// For testing Test, } @@ -237,9 +230,6 @@ enum Module { /// list all modules List, - - /// Shrink module image size - Shrink, } #[derive(clap::Subcommand, Debug)] @@ -301,6 +291,10 @@ pub fn run() -> Result<()> { 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 { @@ -321,7 +315,6 @@ pub fn run() -> Result<()> { Module::Disable { id } => module::disable_module(&id), Module::Action { id } => module::run_action(&id), Module::List => module::list_modules(), - Module::Shrink => module::shrink_ksu_images(), } } Commands::Install { magiskboot } => utils::install(magiskboot), @@ -355,15 +348,7 @@ pub fn run() -> Result<()> { Ok(()) } Debug::Su { global_mnt } => crate::su::grant_root(global_mnt), - Debug::Mount => init_event::mount_modules_systemlessly(defs::MODULE_DIR), - Debug::Xcp { - src, - dst, - punch_hole, - } => { - utils::copy_sparse_file(src, dst, punch_hole)?; - Ok(()) - } + Debug::Mount => init_event::mount_modules_systemlessly(), Debug::Test => assets::ensure_binaries(false), }, diff --git a/userspace/ksud/src/defs.rs b/userspace/ksud/src/defs.rs index ae9dfaa5..976a4d63 100644 --- a/userspace/ksud/src/defs.rs +++ b/userspace/ksud/src/defs.rs @@ -10,7 +10,7 @@ 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 KSU_OVERLAY_SOURCE: &str = "KSU"; +pub const KSU_MOUNT_SOURCE: &str = "KSU"; pub const DAEMON_PATH: &str = concatcp!(ADB_DIR, "ksud"); pub const MAGISKBOOT_PATH: &str = concatcp!(BINARY_DIR, "magiskboot"); @@ -18,25 +18,20 @@ pub const MAGISKBOOT_PATH: &str = concatcp!(BINARY_DIR, "magiskboot"); pub const DAEMON_LINK_PATH: &str = concatcp!(BINARY_DIR, "ksud"); pub const MODULE_DIR: &str = concatcp!(ADB_DIR, "modules/"); -pub const MODULE_IMG: &str = concatcp!(WORKING_DIR, "modules.img"); -pub const MODULE_UPDATE_IMG: &str = concatcp!(WORKING_DIR, "modules_update.img"); - -pub const MODULE_UPDATE_TMP_IMG: &str = concatcp!(WORKING_DIR, "update_tmp.img"); // warning: this directory should not change, or you need to change the code in module_installer.sh!!! -pub const MODULE_UPDATE_TMP_DIR: &str = concatcp!(ADB_DIR, "modules_update/"); +pub const MODULE_UPDATE_DIR: &str = concatcp!(ADB_DIR, "modules_update/"); -pub const SYSTEM_RW_DIR: &str = concatcp!(MODULE_DIR, ".rw/"); +pub const KSUD_VERBOSE_LOG_FILE: &str = concatcp!(ADB_DIR, "verbose"); pub const TEMP_DIR: &str = "/debug_ramdisk"; -pub const TEMP_DIR_LEGACY: &str = "/sbin"; - 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"; pub const SKIP_MOUNT_FILE_NAME: &str = "skip_mount"; +pub const MAGIC_MOUNT_WORK_DIR: &str = concatcp!(TEMP_DIR, "/workdir"); 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")); @@ -44,3 +39,6 @@ pub const VERSION_NAME: &str = include_str!(concat!(env!("OUT_DIR"), "/VERSION_N 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"; + +pub const NO_TMPFS_PATH: &str = concatcp!(WORKING_DIR, ".notmpfs"); +pub const NO_MOUNT_PATH: &str = concatcp!(WORKING_DIR, ".nomount"); diff --git a/userspace/ksud/src/init_event.rs b/userspace/ksud/src/init_event.rs index 7107ff74..36d14522 100644 --- a/userspace/ksud/src/init_event.rs +++ b/userspace/ksud/src/init_event.rs @@ -1,100 +1,10 @@ -use anyhow::{bail, Context, Result}; +use crate::defs::{KSU_MOUNT_SOURCE, NO_MOUNT_PATH, NO_TMPFS_PATH, TEMP_DIR}; +use crate::module::{handle_updated_modules, prune_modules}; +use crate::{assets, defs, ksucalls, restorecon, utils}; +use anyhow::{Context, Result}; use log::{info, warn}; -use std::{collections::HashMap, path::Path}; - -use crate::module::prune_modules; -use crate::{ - assets, defs, ksucalls, mount, restorecon, - utils::{self, ensure_clean_dir}, -}; - -fn mount_partition(partition_name: &str, lowerdir: &Vec) -> Result<()> { - if lowerdir.is_empty() { - warn!("partition: {partition_name} lowerdir is empty"); - return Ok(()); - } - - let partition = format!("/{partition_name}"); - - // if /partition is a symlink and linked to /system/partition, then we don't need to overlay it separately - if Path::new(&partition).read_link().is_ok() { - warn!("partition: {partition} is a symlink"); - return Ok(()); - } - - let mut workdir = None; - let mut upperdir = None; - let system_rw_dir = Path::new(defs::SYSTEM_RW_DIR); - if system_rw_dir.exists() { - workdir = Some(system_rw_dir.join(partition_name).join("workdir")); - upperdir = Some(system_rw_dir.join(partition_name).join("upperdir")); - } - - mount::mount_overlay(&partition, lowerdir, workdir, upperdir) -} - -pub fn mount_modules_systemlessly(module_dir: &str) -> Result<()> { - // construct overlay mount params - let dir = std::fs::read_dir(module_dir); - let Ok(dir) = dir else { - bail!("open {} failed", defs::MODULE_DIR); - }; - - let mut system_lowerdir: Vec = Vec::new(); - - let partition = vec!["vendor", "product", "system_ext", "odm", "oem"]; - let mut partition_lowerdir: HashMap> = HashMap::new(); - for ele in &partition { - partition_lowerdir.insert((*ele).to_string(), Vec::new()); - } - - for entry in dir.flatten() { - let module = entry.path(); - if !module.is_dir() { - continue; - } - let disabled = module.join(defs::DISABLE_FILE_NAME).exists(); - if disabled { - info!("module: {} is disabled, ignore!", module.display()); - continue; - } - let skip_mount = module.join(defs::SKIP_MOUNT_FILE_NAME).exists(); - if skip_mount { - info!("module: {} skip_mount exist, skip!", module.display()); - continue; - } - - let module_system = Path::new(&module).join("system"); - if module_system.is_dir() { - system_lowerdir.push(format!("{}", module_system.display())); - } - - for part in &partition { - // if /partition is a mountpoint, we would move it to $MODPATH/$partition when install - // otherwise it must be a symlink and we don't need to overlay! - let part_path = Path::new(&module).join(part); - if part_path.is_dir() { - if let Some(v) = partition_lowerdir.get_mut(*part) { - v.push(format!("{}", part_path.display())); - } - } - } - } - - // mount /system first - if let Err(e) = mount_partition("system", &system_lowerdir) { - warn!("mount system failed: {:#}", e); - } - - // mount other partitions - for (k, v) in partition_lowerdir { - if let Err(e) = mount_partition(&k, &v) { - warn!("mount {k} failed: {:#}", e); - } - } - - Ok(()) -} +use rustix::fs::{mount, MountFlags}; +use std::path::Path; pub fn on_post_data_fs() -> Result<()> { ksucalls::report_post_fs_data(); @@ -111,11 +21,9 @@ pub fn on_post_data_fs() -> Result<()> { return Ok(()); } - let safe_mode = crate::utils::is_safe_mode(); + let safe_mode = utils::is_safe_mode(); if safe_mode { - // we should still mount modules.img to `/data/adb/modules` in safe mode - // becuase 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 @@ -124,43 +32,8 @@ pub fn on_post_data_fs() -> Result<()> { } } - let module_update_img = defs::MODULE_UPDATE_IMG; - let module_img = defs::MODULE_IMG; - let module_dir = defs::MODULE_DIR; - let module_update_flag = Path::new(defs::WORKING_DIR).join(defs::UPDATE_FILE_NAME); - - // modules.img is the default image - let mut target_update_img = &module_img; - - // we should clean the module mount point if it exists - ensure_clean_dir(module_dir)?; - assets::ensure_binaries(true).with_context(|| "Failed to extract bin assets")?; - if Path::new(module_update_img).exists() { - if module_update_flag.exists() { - // if modules_update.img exists, and the the flag indicate this is an update - // this make sure that if the update failed, we will fallback to the old image - // if we boot succeed, we will rename the modules_update.img to modules.img #on_boot_complete - target_update_img = &module_update_img; - // And we should delete the flag immediately - std::fs::remove_file(module_update_flag)?; - } else { - // if modules_update.img exists, but the flag not exist, we should delete it - std::fs::remove_file(module_update_img)?; - } - } - - if !Path::new(target_update_img).exists() { - return Ok(()); - } - - // we should always mount the module.img to module dir - // becuase we may need to operate the module dir in safe mode - info!("mount module image: {target_update_img} to {module_dir}"); - mount::AutoMountExt4::try_new(target_update_img, module_dir, false) - .with_context(|| "mount module image failed".to_string())?; - // tell kernel that we've mount the module, so that it can do some optimization ksucalls::report_module_mounted(); @@ -177,6 +50,10 @@ pub fn on_post_data_fs() -> Result<()> { warn!("prune modules failed: {}", e); } + if let Err(e) = handle_updated_modules() { + warn!("handle updated modules failed: {}", e); + } + if let Err(e) = restorecon::restorecon() { warn!("restorecon failed: {}", e); } @@ -191,8 +68,12 @@ pub fn on_post_data_fs() -> Result<()> { } // mount temp dir - if let Err(e) = mount::mount_tmpfs(utils::get_tmp_path()) { - warn!("do temp dir mount failed: {}", e); + if !Path::new(NO_TMPFS_PATH).exists() { + if let Err(e) = mount(KSU_MOUNT_SOURCE, TEMP_DIR, "tmpfs", MountFlags::empty(), "") { + warn!("do temp dir mount failed: {}", e); + } + } else { + info!("no tmpfs requested"); } // exec modules post-fs-data scripts @@ -206,15 +87,27 @@ pub fn on_post_data_fs() -> Result<()> { warn!("load system.prop failed: {}", e); } - // mount module systemlessly by overlay - if let Err(e) = mount_modules_systemlessly(module_dir) { - warn!("do systemless mount failed: {}", e); + // mount module systemlessly by magic mount + if !Path::new(NO_MOUNT_PATH).exists() { + if let Err(e) = mount_modules_systemlessly() { + warn!("do systemless mount failed: {}", e); + } + } else { + info!("no mount requested"); } run_stage("post-mount", true); - std::env::set_current_dir("/").with_context(|| "failed to chdir to /")?; + Ok(()) +} +#[cfg(target_os = "android")] +pub fn mount_modules_systemlessly() -> Result<()> { + crate::magic_mount::magic_mount() +} + +#[cfg(not(target_os = "android"))] +pub fn mount_modules_systemlessly() -> Result<()> { Ok(()) } @@ -249,17 +142,6 @@ pub fn on_services() -> Result<()> { pub fn on_boot_completed() -> Result<()> { ksucalls::report_boot_complete(); info!("on_boot_completed triggered!"); - let module_update_img = Path::new(defs::MODULE_UPDATE_IMG); - let module_img = Path::new(defs::MODULE_IMG); - if module_update_img.exists() { - // this is a update and we successfully booted - if std::fs::rename(module_update_img, module_img).is_err() { - warn!("Failed to rename images, copy it now.",); - utils::copy_sparse_file(module_update_img, module_img, false) - .with_context(|| "Failed to copy images")?; - std::fs::remove_file(module_update_img).with_context(|| "Failed to remove image!")?; - } - } run_stage("boot-completed", false); diff --git a/userspace/ksud/src/installer.sh b/userspace/ksud/src/installer.sh index 40138ae3..2ebdddc7 100644 --- a/userspace/ksud/src/installer.sh +++ b/userspace/ksud/src/installer.sh @@ -85,7 +85,7 @@ setup_flashable() { $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 + for FD in /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 @@ -302,18 +302,16 @@ is_legacy_script() { } handle_partition() { - # if /system/vendor is a symlink, we need to move it out of $MODPATH/system, otherwise it will be overlayed - # if /system/vendor is a normal directory, it is ok to overlay it and we don't need to overlay it separately. - if [ ! -e $MODPATH/system/$1 ]; then + PARTITION="$1" + REQUIRE_SYMLINK="$2" + if [ ! -e "$MODPATH/system/$PARTITION" ]; then # no partition found return; fi - if [ -L "/system/$1" ] && [ "$(readlink -f /system/$1)" = "/$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 + if [ "$REQUIRE_SYMLINK" = "false" ] || [ -L "/system/$PARTITION" ] && [ "$(readlink -f "/system/$PARTITION")" = "/$PARTITION" ]; then + ui_print "- Handle partition /$PARTITION" + ln -sf "./system/$PARTITION" "$MODPATH/$PARTITION" fi } @@ -391,22 +389,23 @@ install_module() { [ -f $MODPATH/customize.sh ] && . $MODPATH/customize.sh fi + handle_partition vendor true + handle_partition system_ext true + handle_partition product true + handle_partition odm false + # Handle replace folders for TARGET in $REPLACE; do ui_print "- Replace target: $TARGET" - mark_replace $MODPATH$TARGET + mark_replace "$MODPATH$TARGET" done # Handle remove files for TARGET in $REMOVE; do ui_print "- Remove target: $TARGET" - mark_remove $MODPATH$TARGET + mark_remove "$MODPATH$TARGET" done - handle_partition vendor - handle_partition system_ext - handle_partition product - if $BOOTMODE; then mktouch $NVBASE/modules/$MODID/update rm -rf $NVBASE/modules/$MODID/remove 2>/dev/null diff --git a/userspace/ksud/src/magic_mount.rs b/userspace/ksud/src/magic_mount.rs new file mode 100644 index 00000000..c6bd9f18 --- /dev/null +++ b/userspace/ksud/src/magic_mount.rs @@ -0,0 +1,434 @@ +use crate::defs::{ + DISABLE_FILE_NAME, KSU_MOUNT_SOURCE, MAGIC_MOUNT_WORK_DIR, MODULE_DIR, SKIP_MOUNT_FILE_NAME, +}; +use crate::magic_mount::NodeFileType::{Directory, RegularFile, Symlink, Whiteout}; +use crate::restorecon::{lgetfilecon, lsetfilecon}; +use crate::utils::ensure_dir_exists; +use anyhow::{bail, Context, Result}; +use extattr::lgetxattr; +use rustix::fs::{ + bind_mount, chmod, chown, mount, move_mount, unmount, Gid, MetadataExt, Mode, MountFlags, + MountPropagationFlags, Uid, UnmountFlags, +}; +use rustix::mount::mount_change; +use rustix::path::Arg; +use std::cmp::PartialEq; +use std::collections::hash_map::Entry; +use std::collections::HashMap; +use std::fs; +use std::fs::{create_dir, create_dir_all, read_dir, read_link, DirEntry, FileType}; +use std::os::unix::fs::{symlink, FileTypeExt}; +use std::path::{Path, PathBuf}; + +const REPLACE_DIR_XATTR: &str = "trusted.overlay.opaque"; + +#[derive(PartialEq, Eq, Hash, Clone, Debug)] +enum NodeFileType { + RegularFile, + Directory, + Symlink, + Whiteout, +} + +impl NodeFileType { + fn from_file_type(file_type: FileType) -> Option { + if file_type.is_file() { + Some(RegularFile) + } else if file_type.is_dir() { + Some(Directory) + } else if file_type.is_symlink() { + Some(Symlink) + } else { + None + } + } +} + +#[derive(Debug)] +struct Node { + name: String, + file_type: NodeFileType, + children: HashMap, + // the module that owned this node + module_path: Option, + replace: bool, + skip: bool, +} + +impl Node { + fn collect_module_files>(&mut self, module_dir: T) -> Result { + let dir = module_dir.as_ref(); + let mut has_file = false; + for entry in dir.read_dir()?.flatten() { + let name = entry.file_name().to_string_lossy().to_string(); + + let node = match self.children.entry(name.clone()) { + Entry::Occupied(o) => Some(o.into_mut()), + Entry::Vacant(v) => Self::new_module(&name, &entry).map(|it| v.insert(it)), + }; + + if let Some(node) = node { + has_file |= if node.file_type == Directory { + node.collect_module_files(dir.join(&node.name))? || node.replace + } else { + true + } + } + } + + Ok(has_file) + } + + fn new_root(name: T) -> Self { + Node { + name: name.to_string(), + file_type: Directory, + children: Default::default(), + module_path: None, + replace: false, + skip: false, + } + } + + fn new_module(name: T, entry: &DirEntry) -> Option { + if let Ok(metadata) = entry.metadata() { + let path = entry.path(); + let file_type = if metadata.file_type().is_char_device() && metadata.rdev() == 0 { + Some(Whiteout) + } else { + NodeFileType::from_file_type(metadata.file_type()) + }; + if let Some(file_type) = file_type { + let mut replace = false; + if file_type == Directory { + if let Ok(v) = lgetxattr(&path, REPLACE_DIR_XATTR) { + if String::from_utf8_lossy(&v) == "y" { + replace = true; + } + } + } + return Some(Node { + name: name.to_string(), + file_type, + children: Default::default(), + module_path: Some(path), + replace, + skip: false, + }); + } + } + + None + } +} + +fn collect_module_files() -> Result> { + let mut root = Node::new_root(""); + let mut system = Node::new_root("system"); + let module_root = Path::new(MODULE_DIR); + let mut has_file = false; + for entry in module_root.read_dir()?.flatten() { + if !entry.file_type()?.is_dir() { + continue; + } + + if entry.path().join(DISABLE_FILE_NAME).exists() + || entry.path().join(SKIP_MOUNT_FILE_NAME).exists() + { + continue; + } + + let mod_system = entry.path().join("system"); + if !mod_system.is_dir() { + continue; + } + + log::debug!("collecting {}", entry.path().display()); + + has_file |= system.collect_module_files(&mod_system)?; + } + + if has_file { + for (partition, require_symlink) in [ + ("vendor", true), + ("system_ext", true), + ("product", true), + ("odm", false), + ] { + let path_of_root = Path::new("/").join(partition); + let path_of_system = Path::new("/system").join(partition); + if path_of_root.is_dir() && (!require_symlink || path_of_system.is_symlink()) { + let name = partition.to_string(); + if let Some(node) = system.children.remove(&name) { + root.children.insert(name, node); + } + } + } + root.children.insert("system".to_string(), system); + Ok(Some(root)) + } else { + Ok(None) + } +} + +fn clone_symlink, Dst: AsRef>(src: Src, dst: Dst) -> Result<()> { + let src_symlink = read_link(src.as_ref())?; + symlink(&src_symlink, dst.as_ref())?; + lsetfilecon(dst.as_ref(), lgetfilecon(src.as_ref())?.as_str())?; + log::debug!( + "clone symlink {} -> {}({})", + dst.as_ref().display(), + dst.as_ref().display(), + src_symlink.display() + ); + Ok(()) +} + +fn mount_mirror, WP: AsRef>( + path: P, + work_dir_path: WP, + entry: &DirEntry, +) -> Result<()> { + let path = path.as_ref().join(entry.file_name()); + let work_dir_path = work_dir_path.as_ref().join(entry.file_name()); + let file_type = entry.file_type()?; + + if file_type.is_file() { + log::debug!( + "mount mirror file {} -> {}", + path.display(), + work_dir_path.display() + ); + fs::File::create(&work_dir_path)?; + bind_mount(&path, &work_dir_path)?; + } else if file_type.is_dir() { + log::debug!( + "mount mirror dir {} -> {}", + path.display(), + work_dir_path.display() + ); + create_dir(&work_dir_path)?; + let metadata = entry.metadata()?; + chmod(&work_dir_path, Mode::from_raw_mode(metadata.mode()))?; + unsafe { + chown( + &work_dir_path, + Some(Uid::from_raw(metadata.uid())), + Some(Gid::from_raw(metadata.gid())), + )?; + } + lsetfilecon(&work_dir_path, lgetfilecon(&path)?.as_str())?; + for entry in read_dir(&path)?.flatten() { + mount_mirror(&path, &work_dir_path, &entry)?; + } + } else if file_type.is_symlink() { + log::debug!( + "create mirror symlink {} -> {}", + path.display(), + work_dir_path.display() + ); + clone_symlink(&path, &work_dir_path)?; + } + + Ok(()) +} + +fn do_magic_mount, WP: AsRef>( + path: P, + work_dir_path: WP, + current: Node, + has_tmpfs: bool, +) -> Result<()> { + let mut current = current; + let path = path.as_ref().join(¤t.name); + let work_dir_path = work_dir_path.as_ref().join(¤t.name); + match current.file_type { + RegularFile => { + let target_path = if has_tmpfs { + fs::File::create(&work_dir_path)?; + &work_dir_path + } else { + &path + }; + if let Some(module_path) = ¤t.module_path { + log::debug!( + "mount module file {} -> {}", + module_path.display(), + work_dir_path.display() + ); + bind_mount(module_path, target_path)?; + } else { + bail!("cannot mount root file {}!", path.display()); + } + } + Symlink => { + if let Some(module_path) = ¤t.module_path { + log::debug!( + "create module symlink {} -> {}", + module_path.display(), + work_dir_path.display() + ); + clone_symlink(module_path, &work_dir_path)?; + } else { + bail!("cannot mount root symlink {}!", path.display()); + } + } + Directory => { + let mut create_tmpfs = !has_tmpfs && current.replace && current.module_path.is_some(); + if !has_tmpfs && !create_tmpfs { + for it in &mut current.children { + let (name, node) = it; + let real_path = path.join(name); + let need = match node.file_type { + Symlink => true, + Whiteout => real_path.exists(), + _ => { + if let Ok(metadata) = real_path.metadata() { + let file_type = NodeFileType::from_file_type(metadata.file_type()) + .unwrap_or(Whiteout); + file_type != node.file_type || file_type == Symlink + } else { + // real path not exists + true + } + } + }; + if need { + if current.module_path.is_none() { + log::error!( + "cannot create tmpfs on {}, ignore: {name}", + path.display() + ); + node.skip = true; + continue; + } + create_tmpfs = true; + break; + } + } + } + + let has_tmpfs = has_tmpfs || create_tmpfs; + + if has_tmpfs { + log::debug!( + "creating tmpfs skeleton for {} at {}", + path.display(), + work_dir_path.display() + ); + create_dir_all(&work_dir_path)?; + let (metadata, path) = if path.exists() { + (path.metadata()?, &path) + } else if let Some(module_path) = ¤t.module_path { + (module_path.metadata()?, module_path) + } else { + bail!("cannot mount root dir {}!", path.display()); + }; + chmod(&work_dir_path, Mode::from_raw_mode(metadata.mode()))?; + unsafe { + chown( + &work_dir_path, + Some(Uid::from_raw(metadata.uid())), + Some(Gid::from_raw(metadata.gid())), + )?; + } + lsetfilecon(&work_dir_path, lgetfilecon(path)?.as_str())?; + } + + if create_tmpfs { + log::debug!( + "creating tmpfs for {} at {}", + path.display(), + work_dir_path.display() + ); + bind_mount(&work_dir_path, &work_dir_path).context("bind self")?; + } + + if path.exists() && !current.replace { + for entry in path.read_dir()?.flatten() { + let name = entry.file_name().to_string_lossy().to_string(); + let result = if let Some(node) = current.children.remove(&name) { + if node.skip { + continue; + } + do_magic_mount(&path, &work_dir_path, node, has_tmpfs) + .with_context(|| format!("magic mount {}/{name}", path.display())) + } else if has_tmpfs { + mount_mirror(&path, &work_dir_path, &entry) + .with_context(|| format!("mount mirror {}/{name}", path.display())) + } else { + Ok(()) + }; + + if let Err(e) = result { + if has_tmpfs { + return Err(e); + } else { + log::error!("mount child {}/{name} failed: {}", path.display(), e); + } + } + } + } + + if current.replace { + if current.module_path.is_none() { + bail!( + "dir {} is declared as replaced but it is root!", + path.display() + ); + } else { + log::debug!("dir {} is replaced", path.display()); + } + } + + for (name, node) in current.children.into_iter() { + if node.skip { + continue; + } + if let Err(e) = do_magic_mount(&path, &work_dir_path, node, has_tmpfs) + .with_context(|| format!("magic mount {}/{name}", path.display())) + { + if has_tmpfs { + return Err(e); + } else { + log::error!("mount child {}/{name} failed: {}", path.display(), e); + } + } + } + + if create_tmpfs { + log::debug!( + "moving tmpfs {} -> {}", + work_dir_path.display(), + path.display() + ); + move_mount(&work_dir_path, &path).context("move self")?; + mount_change(&path, MountPropagationFlags::PRIVATE).context("make self private")?; + } + } + Whiteout => { + log::debug!("file {} is removed", path.display()); + } + } + + Ok(()) +} + +pub fn magic_mount() -> Result<()> { + if let Some(root) = collect_module_files()? { + log::debug!("collected: {:#?}", root); + let tmp_dir = PathBuf::from(MAGIC_MOUNT_WORK_DIR); + ensure_dir_exists(&tmp_dir)?; + mount(KSU_MOUNT_SOURCE, &tmp_dir, "tmpfs", MountFlags::empty(), "").context("mount tmp")?; + mount_change(&tmp_dir, MountPropagationFlags::PRIVATE).context("make tmp private")?; + let result = do_magic_mount("/", &tmp_dir, root, false); + if let Err(e) = unmount(&tmp_dir, UnmountFlags::DETACH) { + log::error!("failed to unmount tmp {}", e); + } + fs::remove_dir(tmp_dir).ok(); + result + } else { + log::info!("no modules to mount, skipping!"); + Ok(()) + } +} diff --git a/userspace/ksud/src/main.rs b/userspace/ksud/src/main.rs index 3b517205..a91fc0cb 100644 --- a/userspace/ksud/src/main.rs +++ b/userspace/ksud/src/main.rs @@ -6,8 +6,9 @@ mod debug; mod defs; mod init_event; mod ksucalls; +#[cfg(target_os = "android")] +mod magic_mount; mod module; -mod mount; mod profile; mod restorecon; mod sepolicy; diff --git a/userspace/ksud/src/module.rs b/userspace/ksud/src/module.rs index cb69d52c..87eee351 100644 --- a/userspace/ksud/src/module.rs +++ b/userspace/ksud/src/module.rs @@ -1,9 +1,9 @@ #[allow(clippy::wildcard_imports)] use crate::utils::*; use crate::{ - assets, defs, ksucalls, mount, + assets, defs, ksucalls, restorecon::{restore_syscon, setsyscon}, - sepolicy, utils, + sepolicy, }; use anyhow::{anyhow, bail, ensure, Context, Result}; @@ -12,20 +12,21 @@ use is_executable::is_executable; use java_properties::PropertiesIter; use log::{info, warn}; -use std::fs::OpenOptions; +use std::fs::{copy, rename}; use std::{ collections::HashMap, env::var as env_var, fs::{remove_dir_all, remove_file, set_permissions, File, Permissions}, io::Cursor, path::{Path, PathBuf}, - process::{Command, Stdio}, + process::Command, str::FromStr, }; use zip_extensions::zip_extract_file_to_memory; +use crate::defs::{MODULE_DIR, MODULE_UPDATE_DIR, UPDATE_FILE_NAME}; #[cfg(unix)] -use std::os::unix::{fs::MetadataExt, prelude::PermissionsExt, process::CommandExt}; +use std::os::unix::{prelude::PermissionsExt, process::CommandExt}; const INSTALLER_CONTENT: &str = include_str!("./installer.sh"); const INSTALL_MODULE_SCRIPT: &str = concatcp!( @@ -57,6 +58,7 @@ fn exec_install_script(module_file: &str) -> Result<()> { .env("KSU_KERNEL_VER_CODE", ksucalls::get_version().to_string()) .env("KSU_VER", defs::VERSION_NAME) .env("KSU_VER_CODE", defs::VERSION_CODE) + .env("KSU_MAGIC_MOUNT", "true") .env("OUTFD", "1") .env("ZIPFILE", realpath) .status()?; @@ -76,24 +78,34 @@ fn ensure_boot_completed() -> Result<()> { Ok(()) } -fn mark_update() -> Result<()> { - ensure_file_exists(concatcp!(defs::WORKING_DIR, defs::UPDATE_FILE_NAME)) -} - -fn mark_module_state(module: &str, flag_file: &str, create_or_delete: bool) -> Result<()> { - let module_state_file = Path::new(defs::MODULE_DIR).join(module).join(flag_file); - if create_or_delete { +fn mark_module_state(module: &str, flag_file: &str, create: bool) -> Result<()> { + let module_state_file = Path::new(MODULE_DIR).join(module).join(flag_file); + if create { ensure_file_exists(module_state_file) } else { if module_state_file.exists() { - std::fs::remove_file(module_state_file)?; + remove_file(module_state_file)?; } Ok(()) } } -fn foreach_module(active_only: bool, mut f: impl FnMut(&Path) -> Result<()>) -> Result<()> { - let modules_dir = Path::new(defs::MODULE_DIR); +#[derive(PartialEq, Eq)] +enum ModuleType { + All, + Active, + Updated, +} + +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, + }); + if !modules_dir.is_dir() { + warn!("{} is not a directory, skip", modules_dir.display()); + return Ok(()); + } let dir = std::fs::read_dir(modules_dir)?; for entry in dir.flatten() { let path = entry.path(); @@ -102,11 +114,11 @@ fn foreach_module(active_only: bool, mut f: impl FnMut(&Path) -> Result<()>) -> continue; } - if active_only && path.join(defs::DISABLE_FILE_NAME).exists() { + if module_type == ModuleType::Active && path.join(defs::DISABLE_FILE_NAME).exists() { info!("{} is disabled, skip", path.display()); continue; } - if active_only && path.join(defs::REMOVE_FILE_NAME).exists() { + if module_type == ModuleType::Active && path.join(defs::REMOVE_FILE_NAME).exists() { warn!("{} is removed, skip", path.display()); continue; } @@ -118,27 +130,7 @@ fn foreach_module(active_only: bool, mut f: impl FnMut(&Path) -> Result<()>) -> } fn foreach_active_module(f: impl FnMut(&Path) -> Result<()>) -> Result<()> { - foreach_module(true, f) -} - -fn check_image(img: &str) -> Result<()> { - let result = Command::new("e2fsck") - .args(["-yf", img]) - .stdout(Stdio::piped()) - .status() - .with_context(|| format!("Failed to exec e2fsck {img}"))?; - let code = result.code(); - // 0 or 1 is ok - // 0: no error - // 1: file system errors corrected - // https://man7.org/linux/man-pages/man8/e2fsck.8.html - // ensure!( - // code == Some(0) || code == Some(1), - // "Failed to check image, e2fsck exit code: {}", - // code.unwrap_or(-1) - // ); - info!("e2fsck exit code: {}", code.unwrap_or(-1)); - Ok(()) + foreach_module(ModuleType::Active, f) } pub fn load_sepolicy_rule() -> Result<()> { @@ -183,6 +175,7 @@ fn exec_script>(path: T, wait: bool) -> Result<()> { .env("KSU_KERNEL_VER_CODE", ksucalls::get_version().to_string()) .env("KSU_VER_CODE", defs::VERSION_CODE) .env("KSU_VER", defs::VERSION_NAME) + .env("KSU_MAGIC_MOUNT", "true") .env( "PATH", format!( @@ -258,370 +251,153 @@ pub fn load_system_prop() -> Result<()> { } pub fn prune_modules() -> Result<()> { - foreach_module(false, |module| { - remove_file(module.join(defs::UPDATE_FILE_NAME)).ok(); + foreach_module(ModuleType::All, |module| { + if module.join(defs::REMOVE_FILE_NAME).exists() { + info!("remove module: {}", module.display()); - if !module.join(defs::REMOVE_FILE_NAME).exists() { - return Ok(()); - } - - info!("remove module: {}", module.display()); - - let uninstaller = module.join("uninstall.sh"); - if uninstaller.exists() { - if let Err(e) = exec_script(uninstaller, true) { - warn!("Failed to exec uninstaller: {}", e); + let uninstaller = module.join("uninstall.sh"); + if uninstaller.exists() { + if let Err(e) = exec_script(uninstaller, true) { + warn!("Failed to exec uninstaller: {}", e); + } } - } - if let Err(e) = remove_dir_all(module) { - warn!("Failed to remove {}: {}", module.display(), e); + if let Err(e) = remove_dir_all(module) { + warn!("Failed to remove {}: {}", module.display(), e); + } + } else { + remove_file(module.join(defs::UPDATE_FILE_NAME)).ok(); } - Ok(()) })?; Ok(()) } -fn create_module_image(image: &str, image_size: u64, journal_size: u64) -> Result<()> { - File::create(image) - .context("Failed to create ext4 image file")? - .set_len(image_size) - .context("Failed to truncate ext4 image")?; +pub fn handle_updated_modules() -> Result<()> { + let modules_root = Path::new(MODULE_DIR); + foreach_module(ModuleType::Updated, |module| { + if !module.is_dir() { + return Ok(()); + } - // format the img to ext4 filesystem - let result = Command::new("mkfs.ext4") - .arg("-J") - .arg(format!("size={journal_size}")) - .arg(image) - .stdout(Stdio::piped()) - .output()?; - ensure!( - result.status.success(), - "Failed to format ext4 image: {}", - String::from_utf8(result.stderr).unwrap() - ); - check_image(image)?; - 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 workding 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 faild if will return early. - let mut buffer: Vec = 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(); - - let modules_img = Path::new(defs::MODULE_IMG); - let modules_update_img = Path::new(defs::MODULE_UPDATE_IMG); - let module_update_tmp_dir = defs::MODULE_UPDATE_TMP_DIR; - - let modules_img_exist = modules_img.exists(); - let modules_update_img_exist = modules_update_img.exists(); - - // prepare the tmp module img - let tmp_module_img = defs::MODULE_UPDATE_TMP_IMG; - let tmp_module_path = Path::new(tmp_module_img); - if tmp_module_path.exists() { - std::fs::remove_file(tmp_module_path)?; - } - - let zip_uncompressed_size = get_zip_uncompressed_size(zip)?; - - info!( - "zip uncompressed size: {}", - humansize::format_size(zip_uncompressed_size, humansize::DECIMAL) - ); - - println!("- Preparing image"); - println!( - "- Module size: {}", - humansize::format_size(zip_uncompressed_size, humansize::DECIMAL) - ); - - let sparse_image_size = 1 << 34; // 16GB - let journal_size = 8; // 8M - if !modules_img_exist && !modules_update_img_exist { - // if no modules and modules_update, it is brand new installation, we should create a new img - // create a tmp module img and mount it to modules_update - info!("Creating brand new module image"); - create_module_image(tmp_module_img, sparse_image_size, journal_size)?; - } else if modules_update_img_exist { - // modules_update.img exists, we should use it as tmp img - info!("Using existing modules_update.img as tmp image"); - utils::copy_sparse_file(modules_update_img, tmp_module_img, true).with_context(|| { - format!( - "Failed to copy {} to {}", - modules_update_img.display(), - tmp_module_img - ) - })?; - } else { - // modules.img exists, we should use it as tmp img - info!("Using existing modules.img as tmp image"); - - #[cfg(unix)] - let blksize = std::fs::metadata(defs::MODULE_DIR)?.blksize(); - #[cfg(not(unix))] - let blksize = 0; - // legacy image, it's block size is 1024 with unlimited journal size - if blksize == 1024 { - println!("- Legacy image, migrating to new format, please be patient..."); - create_module_image(tmp_module_img, sparse_image_size, journal_size)?; - let _dontdrop = - mount::AutoMountExt4::try_new(tmp_module_img, module_update_tmp_dir, true) - .with_context(|| format!("Failed to mount {tmp_module_img}"))?; - utils::copy_module_files(defs::MODULE_DIR, module_update_tmp_dir) - .with_context(|| "Failed to migrate module files".to_string())?; - } else { - utils::copy_sparse_file(modules_img, tmp_module_img, true) - .with_context(|| "Failed to copy module image".to_string())?; - - if std::fs::metadata(tmp_module_img)?.len() < sparse_image_size { - // truncate the file to new size - OpenOptions::new() - .write(true) - .open(tmp_module_img) - .context("Failed to open ext4 image")? - .set_len(sparse_image_size) - .context("Failed to truncate ext4 image")?; - - // resize the image to new size - check_image(tmp_module_img)?; - Command::new("resize2fs") - .arg(tmp_module_img) - .stdout(Stdio::piped()) - .status()?; + if let Some(name) = module.file_name() { + let old_dir = modules_root.join(name); + if old_dir.exists() { + if let Err(e) = remove_dir_all(&old_dir) { + log::error!("Failed to remove old {}: {}", old_dir.display(), e); + } + } + if let Err(e) = rename(module, &old_dir) { + log::error!("Failed to move new module {}: {}", module.display(), e); } } - } - - // ensure modules_update exists - ensure_dir_exists(module_update_tmp_dir)?; - - // mount the modules_update.img to mountpoint - println!("- Mounting image"); - - let _dontdrop = mount::AutoMountExt4::try_new(tmp_module_img, module_update_tmp_dir, true)?; - - info!("mounted {} to {}", tmp_module_img, module_update_tmp_dir); - - setsyscon(module_update_tmp_dir)?; - - let module_dir = format!("{module_update_tmp_dir}/{module_id}"); - ensure_clean_dir(&module_dir)?; - info!("module dir: {}", module_dir); - - // unzip the image and move it to modules_update/ dir - let file = File::open(zip)?; - let mut archive = zip::ZipArchive::new(file)?; - archive.extract(&module_dir)?; - - // set permission and selinux context for $MOD/system - let module_system_dir = PathBuf::from(module_dir).join("system"); - if module_system_dir.exists() { - #[cfg(unix)] - set_permissions(&module_system_dir, Permissions::from_mode(0o755))?; - restore_syscon(&module_system_dir)?; - } - - exec_install_script(zip)?; - - info!("rename {tmp_module_img} to {}", defs::MODULE_UPDATE_IMG); - // all done, rename the tmp image to modules_update.img - if std::fs::rename(tmp_module_img, defs::MODULE_UPDATE_IMG).is_err() { - warn!("Rename image failed, try copy it."); - utils::copy_sparse_file(tmp_module_img, defs::MODULE_UPDATE_IMG, true) - .with_context(|| "Failed to copy image.".to_string())?; - let _ = std::fs::remove_file(tmp_module_img); - } - - mark_update()?; - - info!("Module install successfully!"); - + Ok(()) + })?; Ok(()) } pub fn install_module(zip: &str) -> Result<()> { - let result = _install_module(zip); + fn inner(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 = 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(); + + let zip_uncompressed_size = get_zip_uncompressed_size(zip)?; + + info!( + "zip uncompressed size: {}", + humansize::format_size(zip_uncompressed_size, humansize::DECIMAL) + ); + + println!("- Preparing Zip"); + println!( + "- Module size: {}", + humansize::format_size(zip_uncompressed_size, humansize::DECIMAL) + ); + + // ensure modules_update exists + ensure_dir_exists(MODULE_UPDATE_DIR)?; + setsyscon(MODULE_UPDATE_DIR)?; + + let update_module_dir = Path::new(MODULE_UPDATE_DIR).join(module_id); + ensure_clean_dir(&update_module_dir)?; + info!("module dir: {}", update_module_dir.display()); + + let do_install = || -> Result<()> { + // unzip the image and move it to modules_update/ dir + let file = File::open(zip)?; + let mut archive = zip::ZipArchive::new(file)?; + archive.extract(&update_module_dir)?; + + // set permission and selinux context for $MOD/system + let module_system_dir = update_module_dir.join("system"); + if module_system_dir.exists() { + #[cfg(unix)] + set_permissions(&module_system_dir, Permissions::from_mode(0o755))?; + restore_syscon(&module_system_dir)?; + } + + exec_install_script(zip)?; + + let module_dir = Path::new(MODULE_DIR).join(module_id); + ensure_dir_exists(&module_dir)?; + copy( + update_module_dir.join("module.prop"), + module_dir.join("module.prop"), + )?; + ensure_file_exists(module_dir.join(UPDATE_FILE_NAME))?; + + info!("Module install successfully!"); + + Ok(()) + }; + let result = do_install(); + if result.is_err() { + remove_dir_all(&update_module_dir).ok(); + } + result + } + let result = inner(zip); if let Err(ref e) = result { - // error happened, do some cleanup! - let _ = std::fs::remove_file(defs::MODULE_UPDATE_TMP_IMG); - let _ = mount::umount_dir(defs::MODULE_UPDATE_TMP_DIR); println!("- Error: {e}"); } result } -fn update_module(update_dir: &str, id: &str, func: F) -> Result<()> -where - F: Fn(&str, &str) -> Result<()>, -{ - ensure_boot_completed()?; - - let modules_img = Path::new(defs::MODULE_IMG); - let modules_update_img = Path::new(defs::MODULE_UPDATE_IMG); - let modules_update_tmp_img = Path::new(defs::MODULE_UPDATE_TMP_IMG); - if !modules_update_img.exists() && !modules_img.exists() { - bail!("Please install module first!"); - } else if modules_update_img.exists() { - info!( - "copy {} to {}", - modules_update_img.display(), - modules_update_tmp_img.display() - ); - utils::copy_sparse_file(modules_update_img, modules_update_tmp_img, true)?; - } else { - info!( - "copy {} to {}", - modules_img.display(), - modules_update_tmp_img.display() - ); - utils::copy_sparse_file(modules_img, modules_update_tmp_img, true)?; - } - - // ensure modules_update dir exist - ensure_clean_dir(update_dir)?; - - // mount the modules_update img - let _dontdrop = mount::AutoMountExt4::try_new(defs::MODULE_UPDATE_TMP_IMG, update_dir, true)?; - - // call the operation func - let result = func(id, update_dir); - - if let Err(e) = std::fs::rename(modules_update_tmp_img, defs::MODULE_UPDATE_IMG) { - warn!("Rename image failed: {e}, try copy it."); - utils::copy_sparse_file(modules_update_tmp_img, defs::MODULE_UPDATE_IMG, true) - .with_context(|| "Failed to copy image.".to_string())?; - let _ = std::fs::remove_file(modules_update_tmp_img); - } - - mark_update()?; - - result -} - pub fn uninstall_module(id: &str) -> Result<()> { - update_module(defs::MODULE_UPDATE_TMP_DIR, id, |mid, update_dir| { - let dir = Path::new(update_dir); - ensure!(dir.exists(), "No module installed"); - - // iterate the modules_update dir, find the module to be removed - let dir = std::fs::read_dir(dir)?; - for entry in dir.flatten() { - let path = entry.path(); - let module_prop = path.join("module.prop"); - if !module_prop.exists() { - continue; - } - let content = std::fs::read(module_prop)?; - let mut module_id: String = String::new(); - PropertiesIter::new_with_encoding(Cursor::new(content), encoding_rs::UTF_8).read_into( - |k, v| { - if k.eq("id") { - module_id = v; - } - }, - )?; - if module_id.eq(mid) { - let remove_file = path.join(defs::REMOVE_FILE_NAME); - File::create(remove_file).with_context(|| "Failed to create remove file.")?; - break; - } - } - - // santity check - let target_module_path = format!("{update_dir}/{mid}"); - let target_module = Path::new(&target_module_path); - if target_module.exists() { - let remove_file = target_module.join(defs::REMOVE_FILE_NAME); - if !remove_file.exists() { - File::create(remove_file).with_context(|| "Failed to create remove file.")?; - } - } - - let _ = mark_module_state(id, defs::REMOVE_FILE_NAME, true); - - Ok(()) - }) + mark_module_state(id, defs::REMOVE_FILE_NAME, true) } pub fn restore_module(id: &str) -> Result<()> { - update_module(defs::MODULE_UPDATE_TMP_DIR, id, |mid, update_dir| { - let dir = Path::new(update_dir); - ensure!(dir.exists(), "No module directory found"); - - // Iterate over the modules_update directory to find the target module - let dir_entries = std::fs::read_dir(dir)?; - for entry in dir_entries.flatten() { - let path = entry.path(); - let module_prop = path.join("module.prop"); - if !module_prop.exists() { - continue; - } - - let content = std::fs::read(module_prop)?; - let mut module_id: String = String::new(); - PropertiesIter::new_with_encoding(Cursor::new(content), encoding_rs::UTF_8).read_into( - |k, v| { - if k.eq("id") { - module_id = v; - } - }, - )?; - - if module_id.eq(mid) { - let remove_file = path.join(defs::REMOVE_FILE_NAME); - if remove_file.exists() { - std::fs::remove_file(&remove_file) - .with_context(|| "Failed to remove remove file.")?; - } - break; - } - } - - // Sanity check to ensure the module is restored - let target_module_path = format!("{update_dir}/{mid}"); - let target_module = Path::new(&target_module_path); - if target_module.exists() { - let remove_file = target_module.join(defs::REMOVE_FILE_NAME); - if remove_file.exists() { - std::fs::remove_file(&remove_file) - .with_context(|| "Failed to remove remove file.")?; - } - } - - // Reverse the state marking - let _ = mark_module_state(id, defs::REMOVE_FILE_NAME, false); - - Ok(()) - }) + mark_module_state(id, defs::REMOVE_FILE_NAME, false) } pub fn run_action(id: &str) -> Result<()> { @@ -629,37 +405,12 @@ pub fn run_action(id: &str) -> Result<()> { exec_script(&action_script_path, true) } -fn _enable_module(module_dir: &str, mid: &str, enable: bool) -> Result<()> { - let src_module_path = format!("{module_dir}/{mid}"); - let src_module = Path::new(&src_module_path); - ensure!(src_module.exists(), "module: {} not found!", mid); - - let disable_path = src_module.join(defs::DISABLE_FILE_NAME); - if enable { - if disable_path.exists() { - std::fs::remove_file(&disable_path).with_context(|| { - format!("Failed to remove disable file: {}", &disable_path.display()) - })?; - } - } else { - ensure_file_exists(disable_path)?; - } - - let _ = mark_module_state(mid, defs::DISABLE_FILE_NAME, !enable); - - Ok(()) -} - pub fn enable_module(id: &str) -> Result<()> { - update_module(defs::MODULE_UPDATE_TMP_DIR, id, |mid, update_dir| { - _enable_module(update_dir, mid, true) - }) + mark_module_state(id, defs::DISABLE_FILE_NAME, false) } pub fn disable_module(id: &str) -> Result<()> { - update_module(defs::MODULE_UPDATE_TMP_DIR, id, |mid, update_dir| { - _enable_module(update_dir, mid, false) - }) + mark_module_state(id, defs::DISABLE_FILE_NAME, true) } pub fn disable_all_modules() -> Result<()> { @@ -671,8 +422,7 @@ pub fn uninstall_all_modules() -> Result<()> { } 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)?; + let dir = std::fs::read_dir(MODULE_DIR)?; for entry in dir.flatten() { let path = entry.path(); let flag = path.join(flag_file); @@ -712,14 +462,12 @@ fn _list_modules(path: &str) -> Vec> { module_prop_map.insert(k, v); }); + let dir_id = entry.file_name().to_string_lossy().to_string(); + module_prop_map.insert("dir_id".to_owned(), dir_id.clone()); + if !module_prop_map.contains_key("id") || module_prop_map["id"].is_empty() { - if let Some(id) = entry.file_name().to_str() { - info!("Use dir name as module id: {}", id); - module_prop_map.insert("id".to_owned(), id.to_owned()); - } else { - info!("Failed to get module id: {:?}", module_prop); - continue; - } + info!("Use dir name as module id: {dir_id}"); + module_prop_map.insert("id".to_owned(), dir_id.clone()); } // Add enabled, update, remove flags @@ -750,21 +498,3 @@ pub fn list_modules() -> Result<()> { println!("{}", serde_json::to_string_pretty(&modules)?); Ok(()) } - -pub fn shrink_image(img: &str) -> Result<()> { - check_image(img)?; - Command::new("resize2fs") - .arg("-M") - .arg(img) - .stdout(Stdio::piped()) - .status()?; - Ok(()) -} - -pub fn shrink_ksu_images() -> Result<()> { - shrink_image(defs::MODULE_IMG)?; - if Path::new(defs::MODULE_UPDATE_IMG).exists() { - shrink_image(defs::MODULE_UPDATE_IMG)?; - } - Ok(()) -} diff --git a/userspace/ksud/src/restorecon.rs b/userspace/ksud/src/restorecon.rs index 152a7c5e..c396481d 100644 --- a/userspace/ksud/src/restorecon.rs +++ b/userspace/ksud/src/restorecon.rs @@ -61,11 +61,11 @@ pub fn restore_syscon>(dir: P) -> Result<()> { Ok(()) } -fn restore_syscon_if_unlabeled>(dir: P) -> Result<()> { +fn restore_modules_con>(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()) { - if let anyhow::Result::Ok(con) = lgetfilecon(&path) { - if con == UNLABEL_CON || con.is_empty() { + if let Result::Ok(con) = lgetfilecon(&path) { + if con == ADB_CON || con == UNLABEL_CON || con.is_empty() { lsetfilecon(&path, SYSTEM_CON)?; } } @@ -76,6 +76,6 @@ fn restore_syscon_if_unlabeled>(dir: P) -> Result<()> { pub fn restorecon() -> Result<()> { lsetfilecon(defs::DAEMON_PATH, ADB_CON)?; - restore_syscon_if_unlabeled(defs::MODULE_DIR)?; + restore_modules_con(defs::MODULE_DIR)?; Ok(()) } diff --git a/userspace/ksud/src/utils.rs b/userspace/ksud/src/utils.rs index 0f638ace..21033918 100644 --- a/userspace/ksud/src/utils.rs +++ b/userspace/ksud/src/utils.rs @@ -1,26 +1,20 @@ use anyhow::{bail, Context, Error, Ok, Result}; use std::{ - fs::{self, create_dir_all, remove_file, write, File, OpenOptions}, + fs::{create_dir_all, remove_file, write, File, OpenOptions}, io::{ ErrorKind::{AlreadyExists, NotFound}, Write, }, path::Path, process::Command, - sync::OnceLock, }; use crate::{assets, boot_patch, defs, ksucalls, module, restorecon}; -use std::fs::metadata; #[allow(unused_imports)] use std::fs::{set_permissions, Permissions}; #[cfg(unix)] use std::os::unix::prelude::PermissionsExt; -use hole_punch::*; -use std::io::{Read, Seek, SeekFrom}; - -use jwalk::WalkDir; use std::path::PathBuf; #[cfg(any(target_os = "linux", target_os = "android"))] @@ -41,7 +35,7 @@ pub fn ensure_clean_dir(dir: impl AsRef) -> Result<()> { pub fn ensure_file_exists>(file: T) -> Result<()> { match File::options().write(true).create_new(true).open(&file) { - std::result::Result::Ok(_) => Ok(()), + Result::Ok(_) => Ok(()), Err(err) => { if err.kind() == AlreadyExists && file.as_ref().is_file() { Ok(()) @@ -189,68 +183,6 @@ pub fn has_magisk() -> bool { which::which("magisk").is_ok() } -fn is_ok_empty(dir: &str) -> bool { - use std::result::Result::Ok; - - match fs::read_dir(dir) { - Ok(mut entries) => entries.next().is_none(), - Err(_) => false, - } -} - -fn find_temp_path() -> String { - use std::result::Result::Ok; - - if is_ok_empty(defs::TEMP_DIR) { - return defs::TEMP_DIR.to_string(); - } - - // Try to create a random directory in /dev/ - let r = tempfile::tempdir_in("/dev/"); - match r { - Ok(tmp_dir) => { - if let Some(path) = tmp_dir.into_path().to_str() { - return path.to_string(); - } - } - Err(_e) => {} - } - - let dirs = [ - defs::TEMP_DIR, - "/patch_hw", - "/oem", - "/root", - defs::TEMP_DIR_LEGACY, - ]; - - // find empty directory - for dir in dirs { - if is_ok_empty(dir) { - return dir.to_string(); - } - } - - // Fallback to non-empty directory - for dir in dirs { - if metadata(dir).is_ok() { - return dir.to_string(); - } - } - - "".to_string() -} - -pub fn get_tmp_path() -> &'static str { - static CHOSEN_TMP_PATH: OnceLock = OnceLock::new(); - - CHOSEN_TMP_PATH.get_or_init(|| { - let r = find_temp_path(); - log::info!("Chosen temp_path: {}", r); - r - }) -} - #[cfg(target_os = "android")] fn link_ksud_to_bin() -> Result<()> { let ksu_bin = PathBuf::from(defs::DAEMON_PATH); @@ -288,9 +220,7 @@ pub fn uninstall(magiskboot_path: Option) -> Result<()> { println!("- Removing directories.."); std::fs::remove_dir_all(defs::WORKING_DIR).ok(); std::fs::remove_file(defs::DAEMON_PATH).ok(); - crate::mount::umount_dir(defs::MODULE_DIR).ok(); std::fs::remove_dir_all(defs::MODULE_DIR).ok(); - std::fs::remove_dir_all(defs::MODULE_UPDATE_TMP_DIR).ok(); println!("- Restore boot image.."); boot_patch::restore(None, magiskboot_path, true)?; println!("- Uninstall KernelSU manager.."); @@ -302,153 +232,3 @@ pub fn uninstall(magiskboot_path: Option) -> Result<()> { Command::new("reboot").spawn()?; Ok(()) } - -// TODO: use libxcp to improve the speed if cross's MSRV is 1.70 -pub fn copy_sparse_file, Q: AsRef>( - src: P, - dst: Q, - punch_hole: bool, -) -> Result<()> { - let mut src_file = File::open(src.as_ref())?; - let mut dst_file = OpenOptions::new() - .write(true) - .create(true) - .truncate(true) - .open(dst.as_ref())?; - - dst_file.set_len(src_file.metadata()?.len())?; - - let segments = src_file.scan_chunks()?; - for segment in segments { - if let SegmentType::Data = segment.segment_type { - let start = segment.start; - let end = segment.end; - - src_file.seek(SeekFrom::Start(start))?; - dst_file.seek(SeekFrom::Start(start))?; - - let mut buffer = [0; 4096]; - let mut total_bytes_copied = 0; - - while total_bytes_copied < end - start { - let bytes_to_read = - std::cmp::min(buffer.len() as u64, end - start - total_bytes_copied); - let bytes_read = src_file.read(&mut buffer[..bytes_to_read as usize])?; - - if bytes_read == 0 { - break; - } - - if punch_hole && buffer[..bytes_read].iter().all(|&x| x == 0) { - // all zero, don't copy it at all! - dst_file.seek(SeekFrom::Current(bytes_read as i64))?; - total_bytes_copied += bytes_read as u64; - continue; - } - dst_file.write_all(&buffer[..bytes_read])?; - total_bytes_copied += bytes_read as u64; - } - } - } - - Ok(()) -} - -#[cfg(any(target_os = "linux", target_os = "android"))] -fn copy_xattrs(src_path: impl AsRef, dest_path: impl AsRef) -> Result<()> { - use rustix::path::Arg; - let std::result::Result::Ok(xattrs) = extattr::llistxattr(src_path.as_ref()) else { - return Ok(()); - }; - for xattr in xattrs { - let std::result::Result::Ok(value) = extattr::lgetxattr(src_path.as_ref(), &xattr) else { - continue; - }; - log::info!( - "Set {:?} xattr {} = {}", - dest_path.as_ref(), - xattr.to_string_lossy(), - value.to_string_lossy(), - ); - if let Err(e) = - extattr::lsetxattr(dest_path.as_ref(), &xattr, &value, extattr::Flags::empty()) - { - log::warn!("Failed to set xattr: {}", e); - } - } - Ok(()) -} - -#[cfg(any(target_os = "linux", target_os = "android"))] -pub fn copy_module_files(source: impl AsRef, destination: impl AsRef) -> Result<()> { - use rustix::fs::FileTypeExt; - use rustix::fs::MetadataExt; - - for entry in WalkDir::new(source.as_ref()).into_iter() { - let entry = entry.context("Failed to access entry")?; - let source_path = entry.path(); - let relative_path = source_path - .strip_prefix(source.as_ref()) - .context("Failed to generate relative path")?; - let dest_path = destination.as_ref().join(relative_path); - - if let Some(parent) = dest_path.parent() { - std::fs::create_dir_all(parent).context("Failed to create directory")?; - } - - if entry.file_type().is_file() { - std::fs::copy(&source_path, &dest_path).with_context(|| { - format!("Failed to copy file from {source_path:?} to {dest_path:?}",) - })?; - copy_xattrs(&source_path, &dest_path)?; - } else if entry.file_type().is_symlink() { - if dest_path.exists() { - std::fs::remove_file(&dest_path).context("Failed to remove file")?; - } - let target = std::fs::read_link(entry.path()).context("Failed to read symlink")?; - log::info!("Symlink: {:?} -> {:?}", dest_path, target); - std::os::unix::fs::symlink(target, &dest_path).context("Failed to create symlink")?; - copy_xattrs(&source_path, &dest_path)?; - } else if entry.file_type().is_dir() { - create_dir_all(&dest_path)?; - let metadata = std::fs::metadata(&source_path).context("Failed to read metadata")?; - std::fs::set_permissions(&dest_path, metadata.permissions()) - .with_context(|| format!("Failed to set permissions for {dest_path:?}"))?; - copy_xattrs(&source_path, &dest_path)?; - } else if entry.file_type().is_char_device() { - if dest_path.exists() { - std::fs::remove_file(&dest_path).context("Failed to remove file")?; - } - let metadata = std::fs::metadata(&source_path).context("Failed to read metadata")?; - let mode = metadata.permissions().mode(); - let dev = metadata.rdev(); - if dev == 0 { - log::info!( - "Found a char device with major 0: {}", - entry.path().display() - ); - rustix::fs::mknodat( - rustix::fs::CWD, - &dest_path, - rustix::fs::FileType::CharacterDevice, - mode.into(), - dev, - ) - .with_context(|| format!("Failed to create device file at {dest_path:?}"))?; - copy_xattrs(&source_path, &dest_path)?; - } - } else { - log::info!( - "Unknown file type: {:?}, {:?},", - entry.file_type(), - entry.path(), - ); - } - } - Ok(()) -} - -#[cfg(not(any(target_os = "linux", target_os = "android")))] -pub fn copy_module_files(_source: impl AsRef, _destination: impl AsRef) -> Result<()> { - unimplemented!() -}