diff --git a/.github/workflows/build-manager.yml b/.github/workflows/build-manager.yml index 793bf4f0..900f27b7 100644 --- a/.github/workflows/build-manager.yml +++ b/.github/workflows/build-manager.yml @@ -2,7 +2,7 @@ name: Build Manager on: push: - branches: [ "main", "dev", "ci" ] + branches: [ "main", "dev", "ci", "miuix" ] paths: - '.github/workflows/build-manager.yml' - '.github/workflows/build-lkm.yml' @@ -11,13 +11,14 @@ on: - 'userspace/ksud/**' - 'userspace/user_scanner/**' pull_request: - branches: [ "main", "dev" ] + branches: [ "main", "dev", "miuix" ] paths: - '.github/workflows/build-manager.yml' - '.github/workflows/build-lkm.yml' - 'manager/**' - 'kernel/**' - 'userspace/ksud/**' + - 'userspace/user_scanner/**' workflow_call: jobs: diff --git a/.github/workflows/meta-overlay.yml b/.github/workflows/meta-overlay.yml new file mode 100644 index 00000000..bee79910 --- /dev/null +++ b/.github/workflows/meta-overlay.yml @@ -0,0 +1,54 @@ +name: Build meta-overlayfs + +on: + push: + branches: [ "main", "dev", "ci", "miuix" ] + paths: + - '.github/workflows/meta-overlay.yml' + - 'userspace/meta-overlayfs/**' + pull_request: + branches: [ "main", "dev", "miuix" ] + paths: + - '.github/workflows/meta-overlay.yml' + - 'userspace/meta-overlayfs/**' + workflow_call: + +jobs: + build: + runs-on: ubuntu-latest + defaults: + run: + shell: bash + working-directory: userspace/meta-overlayfs + + steps: + - name: Checkout + uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - name: Setup Rust + run: | + rustup update stable + rustup target add aarch64-linux-android + rustup target add x86_64-linux-android + + - name: Cache Cargo + uses: Swatinem/rust-cache@v2 + with: + workspaces: userspace/meta-overlayfs + cache-targets: false + + - name: Install cross + run: | + RUSTFLAGS="" cargo install cross --git https://github.com/cross-rs/cross --rev 66845c1 + + - name: Build meta-overlayfs metamodule + run: chmod +x build.sh && ./build.sh + + - name: Upload artifact + uses: actions/upload-artifact@v5 + with: + name: meta-overlayfs + path: userspace/meta-overlayfs/target/meta-overlayfs-*.zip + if-no-files-found: error diff --git a/userspace/ksud/bin/aarch64/resetprop b/userspace/ksud/bin/aarch64/resetprop index 2dc7d0a4..dd58ca45 100644 Binary files a/userspace/ksud/bin/aarch64/resetprop and b/userspace/ksud/bin/aarch64/resetprop differ diff --git a/userspace/ksud/bin/x86_64/resetprop b/userspace/ksud/bin/x86_64/resetprop index 80030612..9048971a 100644 Binary files a/userspace/ksud/bin/x86_64/resetprop and b/userspace/ksud/bin/x86_64/resetprop differ diff --git a/userspace/ksud/src/cli.rs b/userspace/ksud/src/cli.rs index 70f591a6..c0830eb5 100644 --- a/userspace/ksud/src/cli.rs +++ b/userspace/ksud/src/cli.rs @@ -1,15 +1,13 @@ use anyhow::{Ok, Result}; use clap::Parser; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; #[cfg(target_os = "android")] use android_logger::Config; #[cfg(target_os = "android")] use log::LevelFilter; -use crate::{ - apk_sign, assets, debug, defs, defs::KSUD_VERBOSE_LOG_FILE, init_event, ksucalls, module, utils, -}; +use crate::{apk_sign, assets, debug, defs, init_event, ksucalls, module, utils}; /// KernelSU userspace cli #[derive(Parser, Debug)] @@ -17,9 +15,6 @@ use crate::{ struct Args { #[command(subcommand)] command: Commands, - - #[arg(short, long, default_value_t = cfg!(debug_assertions))] - verbose: bool, } #[derive(clap::Subcommand, Debug)] @@ -209,8 +204,6 @@ enum Debug { /// Get kernel version Version, - Mount, - /// For testing Test, @@ -277,14 +270,14 @@ enum Module { zip: String, }, - /// Uninstall module - Uninstall { + /// Undo module uninstall mark + UndoUninstall { /// module id id: String, }, - /// Restore module - Restore { + /// Uninstall module + Uninstall { /// module id id: String, }, @@ -498,10 +491,6 @@ 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 { @@ -515,8 +504,8 @@ pub fn run() -> Result<()> { } match command { Module::Install { zip } => module::install_module(&zip), + Module::UndoUninstall { id } => module::undo_uninstall_module(&id), Module::Uninstall { id } => module::uninstall_module(&id), - Module::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), @@ -563,7 +552,6 @@ pub fn run() -> Result<()> { 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), @@ -672,11 +660,7 @@ pub fn run() -> Result<()> { }; if let Err(e) = &result { - for c in e.chain() { - log::error!("{c:#?}"); - } - - log::error!("{:#?}", e.backtrace()); + log::error!("Error: {e:?}"); } result } diff --git a/userspace/ksud/src/defs.rs b/userspace/ksud/src/defs.rs index 8033ee26..3ce7eab1 100644 --- a/userspace/ksud/src/defs.rs +++ b/userspace/ksud/src/defs.rs @@ -10,7 +10,6 @@ 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_MOUNT_SOURCE: &str = "KSU"; pub const DAEMON_PATH: &str = concatcp!(ADB_DIR, "ksud"); pub const MAGISKBOOT_PATH: &str = concatcp!(BINARY_DIR, "magiskboot"); @@ -18,18 +17,19 @@ 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/"); - -// warning: this directory should not change, or you need to change the code in module_installer.sh!!! pub const MODULE_UPDATE_DIR: &str = concatcp!(ADB_DIR, "modules_update/"); - -pub const KSUD_VERBOSE_LOG_FILE: &str = concatcp!(ADB_DIR, "verbose"); +pub const METAMODULE_DIR: &str = concatcp!(ADB_DIR, "metamodule/"); pub const MODULE_WEB_DIR: &str = "webroot"; pub const MODULE_ACTION_SH: &str = "action.sh"; pub const DISABLE_FILE_NAME: &str = "disable"; pub const UPDATE_FILE_NAME: &str = "update"; pub const REMOVE_FILE_NAME: &str = "remove"; -pub const SKIP_MOUNT_FILE_NAME: &str = "skip_mount"; + +// Metamodule support +pub const METAMODULE_MOUNT_SCRIPT: &str = "metamount.sh"; +pub const METAMODULE_METAINSTALL_SCRIPT: &str = "metainstall.sh"; +pub const METAMODULE_METAUNINSTALL_SCRIPT: &str = "metauninstall.sh"; pub const VERSION_CODE: &str = include_str!(concat!(env!("OUT_DIR"), "/VERSION_CODE")); pub const VERSION_NAME: &str = include_str!(concat!(env!("OUT_DIR"), "/VERSION_NAME")); @@ -37,6 +37,3 @@ 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 c0f905b5..c2870eb6 100644 --- a/userspace/ksud/src/init_event.rs +++ b/userspace/ksud/src/init_event.rs @@ -1,28 +1,14 @@ -#[cfg(target_arch = "aarch64")] -use crate::kpm; -use crate::utils::is_safe_mode; -use crate::{ - assets, defs, - defs::{KSU_MOUNT_SOURCE, NO_MOUNT_PATH, NO_TMPFS_PATH}, - ksucalls, - module::{handle_updated_modules, prune_modules}, - restorecon, uid_scanner, utils, - utils::find_tmp_path, -}; use anyhow::{Context, Result}; use log::{info, warn}; -use rustix::fs::{MountFlags, mount}; use std::path::Path; - -#[cfg(target_os = "android")] -pub fn mount_modules_systemlessly() -> Result<()> { - crate::magic_mount::magic_mount(&find_tmp_path()) -} - -#[cfg(not(target_os = "android"))] -pub fn mount_modules_systemlessly() -> Result<()> { - Ok(()) -} +#[cfg(target_arch = "aarch64")] +use crate::kpm; +use crate::module::{handle_updated_modules, prune_modules}; +use crate::utils::is_safe_mode; +use crate::{ + assets, defs, ksucalls, metamodule, restorecon, + utils::{self}, +}; pub fn on_post_data_fs() -> Result<()> { ksucalls::report_post_fs_data(); @@ -39,9 +25,11 @@ pub fn on_post_data_fs() -> Result<()> { return Ok(()); } - let safe_mode = utils::is_safe_mode(); + let safe_mode = crate::utils::is_safe_mode(); if safe_mode { + // we should still ensure module directory exists in safe mode + // because we may need to operate the module dir in safe mode warn!("safe mode, skip common post-fs-data.d scripts"); } else { // Then exec common post-fs-data scripts @@ -50,18 +38,18 @@ pub fn on_post_data_fs() -> Result<()> { } } + let module_dir = defs::MODULE_DIR; + assets::ensure_binaries(true).with_context(|| "Failed to extract bin assets")?; // Start UID scanner daemon with highest priority - uid_scanner::start_uid_scanner_daemon()?; + crate::uid_scanner::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(); // if we are in safe mode, we should disable all modules if safe_mode { @@ -72,14 +60,14 @@ pub fn on_post_data_fs() -> Result<()> { return Ok(()); } - if let Err(e) = prune_modules() { - warn!("prune modules failed: {e}"); - } - if let Err(e) = handle_updated_modules() { warn!("handle updated modules failed: {e}"); } + if let Err(e) = prune_modules() { + warn!("prune modules failed: {e}"); + } + if let Err(e) = restorecon::restorecon() { warn!("restorecon failed: {e}"); } @@ -110,23 +98,9 @@ pub fn on_post_data_fs() -> Result<()> { warn!("KPM: Failed to load KPM modules: {e}"); } - let tmpfs_path = find_tmp_path(); - // for compatibility - let no_mount = Path::new(NO_TMPFS_PATH).exists() || Path::new(NO_MOUNT_PATH).exists(); - - // mount temp dir - if !no_mount { - if let Err(e) = mount( - KSU_MOUNT_SOURCE, - &tmpfs_path, - "tmpfs", - MountFlags::empty(), - "", - ) { - warn!("do temp dir mount failed: {e}"); - } - } else { - info!("no tmpfs requested"); + // execute metamodule post-fs-data script first (priority) + if let Err(e) = metamodule::exec_stage_script("post-fs-data", true) { + warn!("exec metamodule post-fs-data script failed: {e}"); } // exec modules post-fs-data scripts @@ -140,18 +114,15 @@ pub fn on_post_data_fs() -> Result<()> { warn!("load system.prop failed: {e}"); } - // mount module systemlessly by magic mount - #[cfg(target_os = "android")] - if !no_mount { - if let Err(e) = crate::magic_mount::magic_mount(&tmpfs_path) { - warn!("do systemless mount failed: {e}"); - } - } else { - info!("no mount requested"); + // execute metamodule mount script + if let Err(e) = metamodule::exec_mount_script(module_dir) { + warn!("execute metamodule mount failed: {e}"); } run_stage("post-mount", true); + std::env::set_current_dir("/").with_context(|| "failed to chdir to /")?; + Ok(()) } @@ -171,6 +142,13 @@ fn run_stage(stage: &str, block: bool) { if let Err(e) = crate::module::exec_common_scripts(&format!("{stage}.d"), block) { warn!("Failed to exec common {stage} scripts: {e}"); } + + // execute metamodule stage script first (priority) + if let Err(e) = metamodule::exec_stage_script(stage, block) { + warn!("Failed to exec metamodule {stage} script: {e}"); + } + + // execute regular modules stage scripts if let Err(e) = crate::module::exec_stage_script(stage, block) { warn!("Failed to exec {stage} scripts: {e}"); } diff --git a/userspace/ksud/src/installer.sh b/userspace/ksud/src/installer.sh index 8ab7e5d5..00ad86d7 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 /proc/$$/fd/*; do + for FD in `ls /proc/$$/fd`; do if readlink /proc/$$/fd/$FD | grep -q pipe; then if ps | grep -v grep | grep -qE " 3 $FD |status_fd=$FD"; then OUTFD=$FD @@ -313,14 +313,6 @@ mark_remove() { chmod 644 $1 } -mark_replace() { - # REPLACE must be directory!!! - # https://docs.kernel.org/filesystems/overlayfs.html#whiteouts-and-opaque-directories - mkdir -p $1 2>/dev/null - setfattr -n trusted.overlay.opaque -v y $1 - chmod 644 $1 -} - request_size_check() { reqSizeM=`du -ms "$1" | cut -f1` } @@ -338,16 +330,19 @@ is_legacy_script() { } handle_partition() { - PARTITION="$1" - REQUIRE_SYMLINK="$2" - if [ ! -e "$MODPATH/system/$PARTITION" ]; then + # if /system/vendor is a symlink, we need to move it out of $MODPATH/system + # if /system/vendor is a normal directory, no special handling is needed. + if [ ! -e $MODPATH/system/$1 ]; then # no partition found return; fi - if [ "$REQUIRE_SYMLINK" = "false" ] || [ -L "/system/$PARTITION" ] && [ "$(readlink -f "/system/$PARTITION")" = "/$PARTITION" ]; then - ui_print "- Handle partition /$PARTITION" - ln -sf "./system/$PARTITION" "$MODPATH/$PARTITION" + # we move the folder to / only if it is a native folder that is not a symlink + if [ -d "/$1" ] && [ ! -L "/$1" ]; then + ui_print "- Handle partition /$1" + # we create a symlink if module want to access $MODPATH/system/$1 + # but it doesn't always work(ie. write it in post-fs-data.sh would fail because it is readonly) + mv -f $MODPATH/system/$1 $MODPATH/$1 && ln -sf ../$1 $MODPATH/system/$1 fi } @@ -428,23 +423,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 + handle_partition odm + 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 deleted file mode 100644 index 8ca539ed..00000000 --- a/userspace/ksud/src/magic_mount.rs +++ /dev/null @@ -1,465 +0,0 @@ -use std::{ - cmp::PartialEq, - collections::{HashMap, hash_map::Entry}, - fs::{self, DirEntry, FileType, create_dir, create_dir_all, read_dir, read_link}, - os::unix::fs::{FileTypeExt, symlink}, - path::{Path, PathBuf}, -}; - -use anyhow::{Context, Result, bail}; -use extattr::lgetxattr; -use rustix::{ - fs::{ - Gid, MetadataExt, Mode, MountFlags, MountPropagationFlags, Uid, UnmountFlags, bind_mount, - chmod, chown, mount, move_mount, remount, unmount, - }, - mount::mount_change, - path::Arg, -}; - -use crate::{ - defs::{DISABLE_FILE_NAME, KSU_MOUNT_SOURCE, MODULE_DIR, SKIP_MOUNT_FILE_NAME}, - magic_mount::NodeFileType::{Directory, RegularFile, Symlink, Whiteout}, - restorecon::{lgetfilecon, lsetfilecon}, - utils::ensure_dir_exists, -}; - -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: P) -> Result - where - P: AsRef, - { - 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 - where - T: ToString, - { - 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 - where - T: ToString, - { - 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 - && let Ok(v) = lgetxattr(&path, REPLACE_DIR_XATTR) - && 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

(src: P, dst: P) -> Result<()> -where - P: AsRef, -{ - 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

(path: P, work_dir_path: P, entry: &DirEntry) -> Result<()> -where - P: AsRef, -{ - 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(path: P, work_dir_path: WP, current: Node, has_tmpfs: bool) -> Result<()> -where - P: AsRef, - WP: AsRef, -{ - 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).with_context(|| { - format!("mount module file {module_path:?} -> {work_dir_path:?}") - })?; - // we should use MS_REMOUNT | MS_BIND | MS_xxx to change mount flags - if let Err(e) = remount(target_path, MountFlags::RDONLY | MountFlags::BIND, "") { - log::warn!("make file {target_path:?} ro: {e:#?}"); - } - } 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).with_context(|| { - format!("create module 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.symlink_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") - .with_context(|| format!("creating tmpfs for {path:?} at {work_dir_path:?}"))?; - } - - 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: {e:#?}", path.display()); - } - } - } - } - - 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: {e:#?}", path.display()); - } - } - } - - if create_tmpfs { - log::debug!( - "moving tmpfs {} -> {}", - work_dir_path.display(), - path.display() - ); - if let Err(e) = remount(&work_dir_path, MountFlags::RDONLY | MountFlags::BIND, "") { - log::warn!("make dir {path:?} ro: {e:#?}"); - } - move_mount(&work_dir_path, &path) - .context("move self") - .with_context(|| format!("moving tmpfs {work_dir_path:?} -> {path:?}"))?; - // make private to reduce peer group count - if let Err(e) = mount_change(&path, MountPropagationFlags::PRIVATE) { - log::warn!("make dir {path:?} private: {e:#?}"); - } - } - } - Whiteout => { - log::debug!("file {} is removed", path.display()); - } - } - - Ok(()) -} - -pub fn magic_mount(tmp_path: &String) -> Result<()> { - if let Some(root) = collect_module_files()? { - log::debug!("collected: {:#?}", root); - let tmp_dir = Path::new(tmp_path).join("workdir"); - 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 b6f32c1e..173f10e5 100644 --- a/userspace/ksud/src/main.rs +++ b/userspace/ksud/src/main.rs @@ -9,13 +9,13 @@ mod init_event; #[cfg(target_arch = "aarch64")] mod kpm; mod ksucalls; -#[cfg(target_os = "android")] -mod magic_mount; +mod metamodule; mod module; mod profile; mod restorecon; mod sepolicy; mod su; +#[cfg(target_os = "android")] mod uid_scanner; mod umount_manager; mod utils; diff --git a/userspace/ksud/src/metamodule.rs b/userspace/ksud/src/metamodule.rs new file mode 100644 index 00000000..a49a9b61 --- /dev/null +++ b/userspace/ksud/src/metamodule.rs @@ -0,0 +1,287 @@ +//! Metamodule management +//! +//! This module handles all metamodule-related functionality. +//! Metamodules are special modules that manage how regular modules are mounted +//! and provide hooks for module installation/uninstallation. + +use anyhow::{Context, Result, ensure}; +use log::{info, warn}; +use std::{ + collections::HashMap, + path::{Path, PathBuf}, + process::Command, +}; + +use crate::module::ModuleType::All; +use crate::{assets, defs}; + +/// Determine whether the provided module properties mark it as a metamodule +pub fn is_metamodule(props: &HashMap) -> bool { + props + .get("metamodule") + .map(|s| { + let trimmed = s.trim(); + trimmed == "1" || trimmed.eq_ignore_ascii_case("true") + }) + .unwrap_or(false) +} + +/// Get metamodule path if it exists +/// The metamodule is stored in /data/adb/modules/{id} with a symlink at /data/adb/metamodule +pub fn get_metamodule_path() -> Option { + let path = Path::new(defs::METAMODULE_DIR); + + // Check if symlink exists and resolve it + if path.is_symlink() + && let Ok(target) = std::fs::read_link(path) + { + // If target is relative, resolve it + let resolved = if target.is_absolute() { + target + } else { + path.parent()?.join(target) + }; + + if resolved.exists() && resolved.is_dir() { + return Some(resolved); + } else { + warn!( + "Metamodule symlink points to non-existent path: {:?}", + resolved + ); + } + } + + // Fallback: search for metamodule=1 in modules directory + let mut result = None; + let _ = crate::module::foreach_module(All, |module_path| { + if let Ok(props) = crate::module::read_module_prop(module_path) + && is_metamodule(&props) + { + info!("Found metamodule in modules directory: {:?}", module_path); + result = Some(module_path.to_path_buf()); + } + Ok(()) + }); + + result +} + +/// Check if metamodule exists +pub fn has_metamodule() -> bool { + get_metamodule_path().is_some() +} + +/// Check if it's safe to install a regular module +/// Returns Ok(()) if safe, Err(is_disabled) if blocked +/// - Err(true) means metamodule is disabled +/// - Err(false) means metamodule is in other unstable state +pub fn check_install_safety() -> Result<(), bool> { + // No metamodule → safe + let Some(metamodule_path) = get_metamodule_path() else { + return Ok(()); + }; + + // No metainstall.sh → safe (uses default installer) + // The staged update directory may contain the latest scripts, so check both locations + let has_metainstall = metamodule_path + .join(defs::METAMODULE_METAINSTALL_SCRIPT) + .exists() + || metamodule_path.file_name().is_some_and(|module_id| { + Path::new(defs::MODULE_UPDATE_DIR) + .join(module_id) + .join(defs::METAMODULE_METAINSTALL_SCRIPT) + .exists() + }); + if !has_metainstall { + return Ok(()); + } + + // Check for marker files + let has_update = metamodule_path.join(defs::UPDATE_FILE_NAME).exists(); + let has_remove = metamodule_path.join(defs::REMOVE_FILE_NAME).exists(); + let has_disable = metamodule_path.join(defs::DISABLE_FILE_NAME).exists(); + + // Stable state (no markers) → safe + if !has_update && !has_remove && !has_disable { + return Ok(()); + } + + // Return true if disabled, false for other unstable states + Err(has_disable && !has_update && !has_remove) +} + +/// Create or update the metamodule symlink +/// Points /data/adb/metamodule -> /data/adb/modules/{module_id} +pub(crate) fn ensure_symlink(module_path: &Path) -> Result<()> { + // METAMODULE_DIR might have trailing slash, so we need to trim it + let symlink_path = Path::new(defs::METAMODULE_DIR.trim_end_matches('/')); + + info!( + "Creating metamodule symlink: {:?} -> {:?}", + symlink_path, module_path + ); + + // Remove existing symlink if it exists + if symlink_path.exists() || symlink_path.is_symlink() { + info!("Removing old metamodule symlink/path"); + if symlink_path.is_symlink() { + std::fs::remove_file(symlink_path).with_context(|| "Failed to remove old symlink")?; + } else { + // Could be a directory, remove it + std::fs::remove_dir_all(symlink_path) + .with_context(|| "Failed to remove old directory")?; + } + } + + // Create symlink + #[cfg(unix)] + std::os::unix::fs::symlink(module_path, symlink_path) + .with_context(|| format!("Failed to create symlink to {:?}", module_path))?; + + info!("Metamodule symlink created successfully"); + Ok(()) +} + +/// Remove the metamodule symlink +pub(crate) fn remove_symlink() -> Result<()> { + let symlink_path = Path::new(defs::METAMODULE_DIR.trim_end_matches('/')); + + if symlink_path.is_symlink() { + std::fs::remove_file(symlink_path) + .with_context(|| "Failed to remove metamodule symlink")?; + info!("Metamodule symlink removed"); + } + + Ok(()) +} + +/// Get the install script content, using metainstall.sh from metamodule if available +/// Returns the script content to be executed +pub(crate) fn get_install_script( + is_metamodule: bool, + installer_content: &str, + install_module_script: &str, +) -> Result { + // Check if there's a metamodule with metainstall.sh + // Only apply this logic for regular modules (not when installing metamodule itself) + let install_script = if !is_metamodule { + if let Some(metamodule_path) = get_metamodule_path() { + if metamodule_path.join(defs::DISABLE_FILE_NAME).exists() { + info!("Metamodule is disabled, using default installer"); + install_module_script.to_string() + } else { + let metainstall_path = metamodule_path.join(defs::METAMODULE_METAINSTALL_SCRIPT); + + if metainstall_path.exists() { + info!("Using metainstall.sh from metamodule"); + let metamodule_content = std::fs::read_to_string(&metainstall_path) + .with_context(|| "Failed to read metamodule metainstall.sh")?; + format!("{}\n{}\nexit 0\n", installer_content, metamodule_content) + } else { + info!("Metamodule exists but has no metainstall.sh, using default installer"); + install_module_script.to_string() + } + } + } else { + info!("No metamodule found, using default installer"); + install_module_script.to_string() + } + } else { + info!("Installing metamodule, using default installer"); + install_module_script.to_string() + }; + + Ok(install_script) +} + +/// Check if metamodule script exists and is ready to execute +/// Returns None if metamodule doesn't exist, is disabled, or script is missing +/// Returns Some(script_path) if script is ready to execute +fn check_metamodule_script(script_name: &str) -> Option { + // Check if metamodule exists + let metamodule_path = get_metamodule_path()?; + + // Check if metamodule is disabled + if metamodule_path.join(defs::DISABLE_FILE_NAME).exists() { + info!("Metamodule is disabled, skipping {}", script_name); + return None; + } + + // Check if script exists + let script_path = metamodule_path.join(script_name); + if !script_path.exists() { + return None; + } + + Some(script_path) +} + +/// Execute metamodule's metauninstall.sh for a specific module +pub(crate) fn exec_metauninstall_script(module_id: &str) -> Result<()> { + let Some(metauninstall_path) = check_metamodule_script(defs::METAMODULE_METAUNINSTALL_SCRIPT) + else { + return Ok(()); + }; + + info!( + "Executing metamodule metauninstall.sh for module: {}", + module_id + ); + + let result = Command::new(assets::BUSYBOX_PATH) + .args(["sh", metauninstall_path.to_str().unwrap()]) + .current_dir(metauninstall_path.parent().unwrap()) + .envs(crate::module::get_common_script_envs()) + .env("MODULE_ID", module_id) + .status()?; + + ensure!( + result.success(), + "Metamodule metauninstall.sh failed for module {}: {:?}", + module_id, + result + ); + + info!( + "Metamodule metauninstall.sh executed successfully for {}", + module_id + ); + Ok(()) +} + +/// Execute metamodule mount script +pub fn exec_mount_script(module_dir: &str) -> Result<()> { + let Some(mount_script) = check_metamodule_script(defs::METAMODULE_MOUNT_SCRIPT) else { + return Ok(()); + }; + + info!("Executing mount script for metamodule"); + + let result = Command::new(assets::BUSYBOX_PATH) + .args(["sh", mount_script.to_str().unwrap()]) + .envs(crate::module::get_common_script_envs()) + .env("MODULE_DIR", module_dir) + .status()?; + + ensure!( + result.success(), + "Metamodule mount script failed with status: {:?}", + result + ); + + info!("Metamodule mount script executed successfully"); + Ok(()) +} + +/// Execute metamodule script for a specific stage +pub fn exec_stage_script(stage: &str, block: bool) -> Result<()> { + let Some(script_path) = check_metamodule_script(&format!("{}.sh", stage)) else { + return Ok(()); + }; + + info!("Executing metamodule {}.sh", stage); + crate::module::exec_script(&script_path, block)?; + info!("Metamodule {}.sh executed successfully", stage); + Ok(()) +} diff --git a/userspace/ksud/src/module.rs b/userspace/ksud/src/module.rs index ce984ac0..edf56647 100644 --- a/userspace/ksud/src/module.rs +++ b/userspace/ksud/src/module.rs @@ -1,14 +1,9 @@ -use std::fs::{copy, rename}; -#[cfg(unix)] -use std::os::unix::{prelude::PermissionsExt, process::CommandExt}; -use std::{ - collections::HashMap, - env::var as env_var, - fs::{File, Permissions, remove_dir_all, remove_file, set_permissions}, - io::Cursor, - path::{Path, PathBuf}, - process::Command, - str::FromStr, +#[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}; @@ -16,17 +11,23 @@ use const_format::concatcp; use is_executable::is_executable; use java_properties::PropertiesIter; use log::{info, warn}; + +use std::fs::{copy, rename}; +use std::{ + collections::HashMap, + env::var as env_var, + fs::{File, Permissions, remove_dir_all, set_permissions}, + io::Cursor, + path::{Path, PathBuf}, + process::Command, + str::FromStr, +}; use zip_extensions::zip_extract_file_to_memory; -#[allow(clippy::wildcard_imports)] -use crate::{ - assets, - defs::{self, MODULE_DIR, MODULE_UPDATE_DIR, UPDATE_FILE_NAME}, - ksucalls, - restorecon::{restore_syscon, setsyscon}, - sepolicy, - utils::*, -}; +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!( @@ -38,27 +39,36 @@ const INSTALL_MODULE_SCRIPT: &str = concatcp!( "\n" ); -fn exec_install_script(module_file: &str) -> Result<()> { - let realpath = std::fs::canonicalize(module_file) - .with_context(|| format!("realpath: {module_file} failed"))?; - - let result = Command::new(assets::BUSYBOX_PATH) - .args(["sh", "-c", INSTALL_MODULE_SCRIPT]) - .env("ASH_STANDALONE", "1") - .env( +/// 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(), + env_var("PATH").unwrap_or_default(), defs::BINARY_DIR.trim_end_matches('/') ), - ) - .env("KSU", "true") - .env("KSU_SUKISU", "true") - .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") + ), + ] +} + +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()?; @@ -66,10 +76,7 @@ fn exec_install_script(module_file: &str) -> Result<()> { Ok(()) } -// becuase we use something like A-B update -// we need to update the module state after the boot_completed -// if someone(such as the module) install a module before the boot_completed -// then it may cause some problems, just forbid it +// 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") { @@ -78,34 +85,21 @@ fn ensure_boot_completed() -> Result<()> { Ok(()) } -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() { - remove_file(module_state_file)?; - } - Ok(()) - } -} - #[derive(PartialEq, Eq)] -enum ModuleType { +pub(crate) enum ModuleType { All, Active, Updated, } -fn foreach_module(module_type: ModuleType, mut f: impl FnMut(&Path) -> Result<()>) -> Result<()> { +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, }); - 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(); @@ -114,11 +108,11 @@ fn foreach_module(module_type: ModuleType, mut f: impl FnMut(&Path) -> Result<() continue; } - if module_type == ModuleType::Active && path.join(defs::DISABLE_FILE_NAME).exists() { + if module_type == Active && path.join(defs::DISABLE_FILE_NAME).exists() { info!("{} is disabled, skip", path.display()); continue; } - if module_type == ModuleType::Active && path.join(defs::REMOVE_FILE_NAME).exists() { + if module_type == Active && path.join(defs::REMOVE_FILE_NAME).exists() { warn!("{} is removed, skip", path.display()); continue; } @@ -130,7 +124,7 @@ fn foreach_module(module_type: ModuleType, mut f: impl FnMut(&Path) -> Result<() } fn foreach_active_module(f: impl FnMut(&Path) -> Result<()>) -> Result<()> { - foreach_module(ModuleType::Active, f) + foreach_module(Active, f) } pub fn load_sepolicy_rule() -> Result<()> { @@ -150,7 +144,7 @@ pub fn load_sepolicy_rule() -> Result<()> { Ok(()) } -fn exec_script>(path: T, wait: bool) -> Result<()> { +pub fn exec_script>(path: T, wait: bool) -> Result<()> { info!("exec {}", path.as_ref().display()); let mut command = &mut Command::new(assets::BUSYBOX_PATH); @@ -169,21 +163,7 @@ fn exec_script>(path: T, wait: bool) -> Result<()> { .current_dir(path.as_ref().parent().unwrap()) .arg("sh") .arg(path.as_ref()) - .env("ASH_STANDALONE", "1") - .env("KSU", "true") - .env("KSU_SUKISU", "true") - .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!( - "{}:{}", - env_var("PATH").unwrap(), - defs::BINARY_DIR.trim_end_matches('/') - ), - ); + .envs(get_common_script_envs()); let result = if wait { command.status().map(|_| ()) @@ -251,45 +231,89 @@ pub fn load_system_prop() -> Result<()> { } pub fn prune_modules() -> Result<()> { - foreach_module(ModuleType::All, |module| { - if module.join(defs::REMOVE_FILE_NAME).exists() { - info!("remove module: {}", module.display()); - - let uninstaller = module.join("uninstall.sh"); - if uninstaller.exists() - && 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); - } - } else { - remove_file(module.join(defs::UPDATE_FILE_NAME)).ok(); + 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}"); + } + + 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, |module| { - if !module.is_dir() { + foreach_module(ModuleType::Updated, |updated_module| { + if !updated_module.is_dir() { return Ok(()); } - if let Some(name) = module.file_name() { - let old_dir = modules_root.join(name); - if old_dir.exists() - && let Err(e) = remove_dir_all(&old_dir) - { - log::error!("Failed to remove old {}: {}", old_dir.display(), e); + 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)?; } - if let Err(e) = rename(module, &old_dir) { - log::error!("Failed to move new module {}: {}", module.display(), e); + 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(()) @@ -297,107 +321,182 @@ pub fn handle_updated_modules() -> Result<()> { Ok(()) } -pub fn install_module(zip: &str) -> Result<()> { - fn inner(zip: &str) -> Result<()> { - ensure_boot_completed()?; +fn _install_module(zip: &str) -> Result<()> { + ensure_boot_completed()?; - // print banner - println!(include_str!("banner")); + // print banner + println!(include_str!("banner")); - assets::ensure_binaries(false).with_context(|| "Failed to extract assets")?; + 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")?; + // 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)?; + // 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 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 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)?; + // Check if this module is a metamodule + let is_metamodule = metamodule::is_metamodule(&module_prop); - 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(); + // 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"); } - result + println!("└─────────────────────────────────\n"); + bail!("Metamodule installation blocked"); } - let result = inner(zip); + + // 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 uninstall_module(id: &str) -> Result<()> { - mark_module_state(id, defs::REMOVE_FILE_NAME, true) +pub fn undo_uninstall_module(id: &str) -> Result<()> { + 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 restore_uninstall_module(id: &str) -> Result<()> { - mark_module_state(id, defs::REMOVE_FILE_NAME, false) +pub fn uninstall_module(id: &str) -> Result<()> { + 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<()> { @@ -406,11 +505,30 @@ pub fn run_action(id: &str) -> Result<()> { } pub fn enable_module(id: &str) -> Result<()> { - mark_module_state(id, defs::DISABLE_FILE_NAME, false) + 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<()> { - mark_module_state(id, defs::DISABLE_FILE_NAME, true) + 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<()> { @@ -418,11 +536,13 @@ pub fn disable_all_modules() -> Result<()> { } 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<()> { - let dir = std::fs::read_dir(MODULE_DIR)?; + // 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); @@ -472,6 +592,7 @@ fn _list_modules(path: &str) -> Vec> { if !path.join("module.prop").exists() { continue; } + let mut module_prop_map = match read_module_prop(&path) { Ok(prop) => prop, Err(e) => { @@ -481,26 +602,33 @@ fn _list_modules(path: &str) -> Vec> { }; // If id is missing or empty, use directory name as fallback - 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() { - info!("Use dir name as module id: {dir_id}"); - module_prop_map.insert("id".to_owned(), dir_id.clone()); + 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 flags + // 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()); modules.push(module_prop_map); } diff --git a/userspace/ksud/src/restorecon.rs b/userspace/ksud/src/restorecon.rs index eb0f35f9..a953a658 100644 --- a/userspace/ksud/src/restorecon.rs +++ b/userspace/ksud/src/restorecon.rs @@ -62,11 +62,11 @@ pub fn restore_syscon>(dir: P) -> Result<()> { Ok(()) } -fn restore_modules_con>(dir: P) -> Result<()> { +fn restore_syscon_if_unlabeled>(dir: P) -> Result<()> { for dir_entry in WalkDir::new(dir).parallelism(Serial) { if let Some(path) = dir_entry.ok().map(|dir_entry| dir_entry.path()) && let Result::Ok(con) = lgetfilecon(&path) - && (con == ADB_CON || con == UNLABEL_CON || con.is_empty()) + && (con == UNLABEL_CON || con.is_empty()) { lsetfilecon(&path, SYSTEM_CON)?; } @@ -76,6 +76,6 @@ fn restore_modules_con>(dir: P) -> Result<()> { pub fn restorecon() -> Result<()> { lsetfilecon(defs::DAEMON_PATH, ADB_CON)?; - restore_modules_con(defs::MODULE_DIR)?; + restore_syscon_if_unlabeled(defs::MODULE_DIR)?; Ok(()) } diff --git a/userspace/ksud/src/utils.rs b/userspace/ksud/src/utils.rs index 307a05ee..5b939443 100644 --- a/userspace/ksud/src/utils.rs +++ b/userspace/ksud/src/utils.rs @@ -1,25 +1,28 @@ -#[cfg(unix)] -use std::os::unix::prelude::PermissionsExt; +use anyhow::{Context, Error, Ok, Result, bail}; use std::{ - fs::{self, File, OpenOptions, create_dir_all, remove_file, write}, - fs::{Permissions, set_permissions}, + fs::{File, OpenOptions, create_dir_all, remove_file, write}, io::{ ErrorKind::{AlreadyExists, NotFound}, Write, }, - path::{Path, PathBuf}, + path::Path, process::Command, }; -use anyhow::{Context, Error, Ok, Result, bail}; +use crate::{assets, boot_patch, defs, ksucalls, module, restorecon}; +#[allow(unused_imports)] +use std::fs::{Permissions, set_permissions}; +#[cfg(unix)] +use std::os::unix::prelude::PermissionsExt; + +use std::path::PathBuf; + #[cfg(any(target_os = "linux", target_os = "android"))] use rustix::{ process, thread::{LinkNameSpaceType, move_into_link_name_space}, }; -use crate::{assets, boot_patch, defs, ksucalls, module, restorecon}; - pub fn ensure_clean_dir(dir: impl AsRef) -> Result<()> { let path = dir.as_ref(); log::debug!("ensure_clean_dir: {}", path.display()); @@ -32,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) { - Result::Ok(_) => Ok(()), + std::result::Result::Ok(_) => Ok(()), Err(err) => { if err.kind() == AlreadyExists && file.as_ref().is_file() { Ok(()) @@ -172,27 +175,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, - } -} - -pub fn find_tmp_path() -> String { - let dirs = ["/debug_ramdisk", "/patch_hw", "/oem", "/root", "/sbin"]; - - // find empty directory - for dir in dirs { - if is_ok_empty(dir) { - return dir.to_string(); - } - } - "".to_string() -} - #[cfg(target_os = "android")] fn link_ksud_to_bin() -> Result<()> { let ksu_bin = PathBuf::from(defs::DAEMON_PATH); diff --git a/userspace/meta-overlayfs/.gitignore b/userspace/meta-overlayfs/.gitignore new file mode 100644 index 00000000..6bfc5c70 --- /dev/null +++ b/userspace/meta-overlayfs/.gitignore @@ -0,0 +1,4 @@ +/target +/out +Cargo.lock +*.log diff --git a/userspace/meta-overlayfs/Cargo.toml b/userspace/meta-overlayfs/Cargo.toml new file mode 100644 index 00000000..820ea2f5 --- /dev/null +++ b/userspace/meta-overlayfs/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "meta-overlayfs" +version = "1.0.0" +edition = "2024" +authors = ["KernelSU Developers"] +description = "An implementation of a metamodule using OverlayFS for KernelSU" +license = "GPL-3.0" + +[dependencies] +anyhow = "1" +log = "0.4" +env_logger = { version = "0.11", default-features = false } + +[target.'cfg(any(target_os = "linux", target_os = "android"))'.dependencies] +rustix = { git = "https://github.com/Kernel-SU/rustix.git", rev = "4a53fbc7cb7a07cabe87125cc21dbc27db316259", features = ["all-apis"] } +procfs = "0.17" + +[profile.release] +strip = true +opt-level = "z" # Minimize binary size +lto = true # Link-time optimization +codegen-units = 1 # Maximum optimization +panic = "abort" # Reduce binary size diff --git a/userspace/meta-overlayfs/README.md b/userspace/meta-overlayfs/README.md new file mode 100644 index 00000000..ace6f528 --- /dev/null +++ b/userspace/meta-overlayfs/README.md @@ -0,0 +1,58 @@ +# meta-overlayfs + +Official overlayfs mount handler for KernelSU metamodules. + +## Installation + +```bash +adb push meta-overlayfs-v1.0.0.zip /sdcard/ +adb shell su -c 'ksud module install /sdcard/meta-overlayfs-v1.0.0.zip' +adb reboot +``` + +Or install via KernelSU Manager → Modules. + +**Note**: The metamodule is now installed as a regular module to `/data/adb/modules/meta-overlay/`, with a symlink created at `/data/adb/metamodule` pointing to it. + +## How It Works + +Uses dual-directory architecture for ext4 image support: + +- **Metadata**: `/data/adb/modules/` - Contains `module.prop`, `disable`, `skip_mount` markers +- **Content**: `/data/adb/metamodule/mnt/` - Contains `system/`, `vendor/` etc. directories from ext4 images + +Scans metadata directory for enabled modules, then mounts their content directories as overlayfs layers. + +### Supported Partitions + +system, vendor, product, system_ext, odm, oem + +### Read-Write Layer + +Optional upperdir/workdir support via `/data/adb/modules/.rw/`: + +```bash +mkdir -p /data/adb/modules/.rw/system/{upperdir,workdir} +``` + +## Environment Variables + +- `MODULE_METADATA_DIR` - Metadata location (default: `/data/adb/modules/`) +- `MODULE_CONTENT_DIR` - Content location (default: `/data/adb/metamodule/mnt/`) +- `RUST_LOG` - Log level (debug, info, warn, error) + +## Architecture + +Automatically selects aarch64 or x86_64 binary during installation (~500KB). + +## Building + +```bash +./build.sh +``` + +Output: `target/meta-overlayfs-v1.0.0.zip` + +## License + +GPL-3.0 diff --git a/userspace/meta-overlayfs/build.sh b/userspace/meta-overlayfs/build.sh new file mode 100644 index 00000000..9f70e8b2 --- /dev/null +++ b/userspace/meta-overlayfs/build.sh @@ -0,0 +1,92 @@ +#!/bin/bash +set -e + +# Configuration +VERSION=$(grep '^version' Cargo.toml | head -1 | sed 's/.*"\(.*\)".*/\1/') +OUTPUT_DIR="target" +METAMODULE_DIR="metamodule" +MODULE_OUTPUT_DIR="$OUTPUT_DIR/module" + +echo "==========================================" +echo "Building meta-overlayfs v${VERSION}" +echo "==========================================" + +# Detect build tool +if command -v cross >/dev/null 2>&1; then + BUILD_TOOL="cross" + echo "Using cross for compilation" +else + BUILD_TOOL="cargo-ndk" + echo "Using cargo ndk for compilation" + if ! command -v cargo-ndk >/dev/null 2>&1; then + echo "Error: Neither cross nor cargo-ndk found!" + echo "Please install one of them:" + echo " - cross: cargo install cross" + echo " - cargo-ndk: cargo install cargo-ndk" + exit 1 + fi +fi + +# Clean output directory +echo "Cleaning output directory..." +rm -rf "$OUTPUT_DIR" +mkdir -p "$MODULE_OUTPUT_DIR" + +# Build for multiple architectures +echo "" +echo "Building for aarch64-linux-android..." +if [ "$BUILD_TOOL" = "cross" ]; then + cross build --release --target aarch64-linux-android +else + cargo ndk build -t arm64-v8a --release +fi + +echo "" +echo "Building for x86_64-linux-android..." +if [ "$BUILD_TOOL" = "cross" ]; then + cross build --release --target x86_64-linux-android +else + cargo ndk build -t x86_64 --release +fi + +# Copy binaries +echo "" +echo "Copying binaries..." +cp target/aarch64-linux-android/release/meta-overlayfs \ + "$MODULE_OUTPUT_DIR/meta-overlayfs-aarch64" +cp target/x86_64-linux-android/release/meta-overlayfs \ + "$MODULE_OUTPUT_DIR/meta-overlayfs-x86_64" + +# Copy metamodule files +echo "Copying metamodule files..." +cp "$METAMODULE_DIR"/module.prop "$MODULE_OUTPUT_DIR/" +cp "$METAMODULE_DIR"/*.sh "$MODULE_OUTPUT_DIR/" + +# Set permissions +echo "Setting permissions..." +chmod 755 "$MODULE_OUTPUT_DIR"/*.sh +chmod 755 "$MODULE_OUTPUT_DIR"/meta-overlayfs-* + +# Display binary sizes +echo "" +echo "Binary sizes:" +echo " aarch64: $(du -h "$MODULE_OUTPUT_DIR"/meta-overlayfs-aarch64 | awk '{print $1}')" +echo " x86_64: $(du -h "$MODULE_OUTPUT_DIR"/meta-overlayfs-x86_64 | awk '{print $1}')" + +# Package +echo "" +echo "Packaging..." +cd "$MODULE_OUTPUT_DIR" +ZIP_NAME="meta-overlayfs-v${VERSION}.zip" +zip -r "../$ZIP_NAME" . +cd ../.. + +echo "" +echo "==========================================" +echo "Build completed successfully!" +echo "Output: $OUTPUT_DIR/$ZIP_NAME" +echo "==========================================" +echo "" +echo "To install:" +echo " adb push $OUTPUT_DIR/$ZIP_NAME /sdcard/" +echo " adb shell su -c 'ksud module install /sdcard/$ZIP_NAME'" diff --git a/userspace/meta-overlayfs/metamodule/.gitkeep b/userspace/meta-overlayfs/metamodule/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/userspace/meta-overlayfs/metamodule/customize.sh b/userspace/meta-overlayfs/metamodule/customize.sh new file mode 100644 index 00000000..feb8686b --- /dev/null +++ b/userspace/meta-overlayfs/metamodule/customize.sh @@ -0,0 +1,77 @@ +#!/system/bin/sh + +ui_print "- Detecting device architecture..." + +# Detect architecture using ro.product.cpu.abi +ABI=$(grep_get_prop ro.product.cpu.abi) +ui_print "- Detected ABI: $ABI" + +# Select the correct binary based on architecture +case "$ABI" in + arm64-v8a) + ARCH_BINARY="meta-overlayfs-aarch64" + REMOVE_BINARY="meta-overlayfs-x86_64" + ui_print "- Selected architecture: ARM64" + ;; + x86_64) + ARCH_BINARY="meta-overlayfs-x86_64" + REMOVE_BINARY="meta-overlayfs-aarch64" + ui_print "- Selected architecture: x86_64" + ;; + *) + abort "! Unsupported architecture: $ABI" + ;; +esac + +# Verify the selected binary exists +if [ ! -f "$MODPATH/$ARCH_BINARY" ]; then + abort "! Binary not found: $ARCH_BINARY" +fi + +ui_print "- Installing $ARCH_BINARY as meta-overlayfs" + +# Rename the selected binary to the generic name +mv "$MODPATH/$ARCH_BINARY" "$MODPATH/meta-overlayfs" || abort "! Failed to rename binary" + +# Remove the unused binary +rm -f "$MODPATH/$REMOVE_BINARY" + +# Ensure the binary is executable +chmod 755 "$MODPATH/meta-overlayfs" || abort "! Failed to set permissions" + +ui_print "- Architecture-specific binary installed successfully" + +# Create ext4 image for module content storage +IMG_FILE="$MODPATH/modules.img" +MNT_DIR="$MODPATH/mnt" +IMG_SIZE_MB=2048 + +if [ ! -f "$IMG_FILE" ]; then + ui_print "- Creating 2GB ext4 image for module storage" + + # Create sparse file (2GB logical size, 0 bytes actual) + truncate -s ${IMG_SIZE_MB}M "$IMG_FILE" || \ + abort "! Failed to create image file" + + # Format as ext4 with small journal (8MB) for safety with minimal overhead + /system/bin/mke2fs -t ext4 -J size=8 -F "$IMG_FILE" >/dev/null 2>&1 || \ + abort "! Failed to format ext4 image" + + ui_print "- Image created successfully (sparse file)" +else + ui_print "- Existing image found, keeping it" +fi + +# Mount image immediately for use +ui_print "- Mounting image for immediate use..." +mkdir -p "$MNT_DIR" +if ! mountpoint -q "$MNT_DIR" 2>/dev/null; then + mount -t ext4 -o loop,rw,noatime "$IMG_FILE" "$MNT_DIR" || \ + abort "! Failed to mount image" + ui_print "- Image mounted successfully" +else + ui_print "- Image already mounted" +fi + +ui_print "- Installation complete" +ui_print "- Image is ready for module installations" diff --git a/userspace/meta-overlayfs/metamodule/metainstall.sh b/userspace/meta-overlayfs/metamodule/metainstall.sh new file mode 100644 index 00000000..7446b6b4 --- /dev/null +++ b/userspace/meta-overlayfs/metamodule/metainstall.sh @@ -0,0 +1,61 @@ +#!/system/bin/sh +############################################ +# meta-overlayfs metainstall.sh +# Module installation hook for ext4 image support +############################################ + +# Constants +IMG_FILE="/data/adb/metamodule/modules.img" +MNT_DIR="/data/adb/metamodule/mnt" + +# Ensure ext4 image is mounted +ensure_image_mounted() { + if ! mountpoint -q "$MNT_DIR" 2>/dev/null; then + ui_print "- Mounting modules image" + mkdir -p "$MNT_DIR" + mount -t ext4 -o loop,rw,noatime "$IMG_FILE" "$MNT_DIR" || { + abort "! Failed to mount modules image" + } + ui_print "- Image mounted successfully" + else + ui_print "- Image already mounted" + fi +} + +# Post-installation: move partition directories to ext4 image +post_install_to_image() { + ui_print "- Moving module content to image" + + MOD_IMG_DIR="$MNT_DIR/$MODID" + mkdir -p "$MOD_IMG_DIR" + + # Move all partition directories + for partition in system vendor product system_ext odm oem; do + if [ -d "$MODPATH/$partition" ]; then + ui_print " Moving $partition/" + mv "$MODPATH/$partition" "$MOD_IMG_DIR/" || { + ui_print "! Warning: Failed to move $partition" + } + fi + done + + # Set permissions + chown -R 0:0 "$MOD_IMG_DIR" 2>/dev/null + chmod -R 755 "$MOD_IMG_DIR" 2>/dev/null + + ui_print "- Module content moved to image" +} + +# Main installation flow +ui_print "- Using meta-overlayfs metainstall" + +# 1. Ensure ext4 image is mounted +ensure_image_mounted + +# 2. Call standard install_module function (defined in installer.sh) +install_module + +# 3. Post-process: move content to image +post_install_to_image + +ui_print "- Installation complete" diff --git a/userspace/meta-overlayfs/metamodule/metamount.sh b/userspace/meta-overlayfs/metamodule/metamount.sh new file mode 100644 index 00000000..de6a6151 --- /dev/null +++ b/userspace/meta-overlayfs/metamodule/metamount.sh @@ -0,0 +1,65 @@ +#!/system/bin/sh +# meta-overlayfs Module Mount Handler +# This script is the entry point for dual-directory module mounting + +MODDIR="${0%/*}" +IMG_FILE="$MODDIR/modules.img" +MNT_DIR="$MODDIR/mnt" + +# Log function +log() { + echo "[meta-overlayfs] $1" +} + +log "Starting module mount process" + +# Ensure ext4 image is mounted +if ! mountpoint -q "$MNT_DIR" 2>/dev/null; then + log "Image not mounted, mounting now..." + + # Check if image file exists + if [ ! -f "$IMG_FILE" ]; then + log "ERROR: Image file not found at $IMG_FILE" + exit 1 + fi + + # Create mount point + mkdir -p "$MNT_DIR" + + # Mount the ext4 image + mount -t ext4 -o loop,rw,noatime "$IMG_FILE" "$MNT_DIR" || { + log "ERROR: Failed to mount image" + exit 1 + } + log "Image mounted successfully at $MNT_DIR" +else + log "Image already mounted at $MNT_DIR" +fi + +# Binary path (architecture-specific binary selected during installation) +BINARY="$MODDIR/meta-overlayfs" + +if [ ! -f "$BINARY" ]; then + log "ERROR: Binary not found: $BINARY" + exit 1 +fi + +# Set dual-directory environment variables +export MODULE_METADATA_DIR="/data/adb/modules" +export MODULE_CONTENT_DIR="$MNT_DIR" + +log "Metadata directory: $MODULE_METADATA_DIR" +log "Content directory: $MODULE_CONTENT_DIR" +log "Executing $BINARY" + +# Execute the mount binary +"$BINARY" +EXIT_CODE=$? + +if [ $EXIT_CODE -ne 0 ]; then + log "Mount failed with exit code $EXIT_CODE" + exit $EXIT_CODE +fi + +log "Mount completed successfully" +exit 0 diff --git a/userspace/meta-overlayfs/metamodule/metauninstall.sh b/userspace/meta-overlayfs/metamodule/metauninstall.sh new file mode 100644 index 00000000..f30df49c --- /dev/null +++ b/userspace/meta-overlayfs/metamodule/metauninstall.sh @@ -0,0 +1,35 @@ +#!/system/bin/sh +############################################ +# mm-overlayfs metauninstall.sh +# Module uninstallation hook for ext4 image cleanup +############################################ + +# Constants +MNT_DIR="/data/adb/metamodule/mnt" + +if [ -z "$MODULE_ID" ]; then + echo "! Error: MODULE_ID not provided" + exit 1 +fi + +echo "- Cleaning up module content from image: $MODULE_ID" + +# Check if image is mounted +if ! mountpoint -q "$MNT_DIR" 2>/dev/null; then + echo "! Warning: Image not mounted, skipping cleanup" + exit 0 +fi + +# Remove module content from image +MOD_IMG_DIR="$MNT_DIR/$MODULE_ID" +if [ -d "$MOD_IMG_DIR" ]; then + echo " Removing $MOD_IMG_DIR" + rm -rf "$MOD_IMG_DIR" || { + echo "! Warning: Failed to remove module content from image" + } + echo "- Module content removed from image" +else + echo "- No module content found in image, skipping" +fi + +exit 0 diff --git a/userspace/meta-overlayfs/metamodule/module.prop b/userspace/meta-overlayfs/metamodule/module.prop new file mode 100644 index 00000000..cb56015e --- /dev/null +++ b/userspace/meta-overlayfs/metamodule/module.prop @@ -0,0 +1,8 @@ +id=meta-overlayfs +metamodule=1 +name=OverlayFS MetaModule +version=1.0.0 +versionCode=1 +author=KernelSU Developers +description=An implementation of a metamodule using OverlayFS for KernelSU +updateJson=https://raw.githubusercontent.com/tiann/KernelSU/main/userspace/meta-overlayfs/update.json diff --git a/userspace/meta-overlayfs/metamodule/uninstall.sh b/userspace/meta-overlayfs/metamodule/uninstall.sh new file mode 100644 index 00000000..90d41d4e --- /dev/null +++ b/userspace/meta-overlayfs/metamodule/uninstall.sh @@ -0,0 +1,24 @@ +#!/system/bin/sh +############################################ +# mm-overlayfs uninstall.sh +# Cleanup script for metamodule removal +############################################ + +MODDIR="${0%/*}" +MNT_DIR="$MODDIR/mnt" + +echo "- Uninstalling metamodule..." + +# Unmount the ext4 image if mounted +if mountpoint -q "$MNT_DIR" 2>/dev/null; then + echo "- Unmounting image..." + umount "$MNT_DIR" 2>/dev/null || { + echo "- Warning: Failed to unmount cleanly" + umount -l "$MNT_DIR" 2>/dev/null + } + echo "- Image unmounted" +fi + +echo "- Uninstall complete" + +exit 0 diff --git a/userspace/meta-overlayfs/src/defs.rs b/userspace/meta-overlayfs/src/defs.rs new file mode 100644 index 00000000..d54c5e67 --- /dev/null +++ b/userspace/meta-overlayfs/src/defs.rs @@ -0,0 +1,17 @@ +// Constants for KernelSU module mounting + +// Dual-directory support for ext4 image +pub const MODULE_METADATA_DIR: &str = "/data/adb/modules/"; +pub const MODULE_CONTENT_DIR: &str = "/data/adb/metamodule/mnt/"; + +// Legacy constant (for backwards compatibility) +pub const _MODULE_DIR: &str = "/data/adb/modules/"; + +// Status marker files +pub const DISABLE_FILE_NAME: &str = "disable"; +pub const _REMOVE_FILE_NAME: &str = "remove"; +pub const SKIP_MOUNT_FILE_NAME: &str = "skip_mount"; + +// System directories +pub const SYSTEM_RW_DIR: &str = "/data/adb/modules/.rw/"; +pub const KSU_OVERLAY_SOURCE: &str = "KSU"; diff --git a/userspace/meta-overlayfs/src/main.rs b/userspace/meta-overlayfs/src/main.rs new file mode 100644 index 00000000..4100e2af --- /dev/null +++ b/userspace/meta-overlayfs/src/main.rs @@ -0,0 +1,29 @@ +use anyhow::Result; +use log::info; + +mod defs; +mod mount; + +fn main() -> Result<()> { + // Initialize logger + env_logger::builder() + .filter_level(log::LevelFilter::Info) + .init(); + + info!("meta-overlayfs v{}", env!("CARGO_PKG_VERSION")); + + // Dual-directory support: metadata + content + let metadata_dir = std::env::var("MODULE_METADATA_DIR") + .unwrap_or_else(|_| defs::MODULE_METADATA_DIR.to_string()); + let content_dir = std::env::var("MODULE_CONTENT_DIR") + .unwrap_or_else(|_| defs::MODULE_CONTENT_DIR.to_string()); + + info!("Metadata directory: {}", metadata_dir); + info!("Content directory: {}", content_dir); + + // Execute dual-directory mounting + mount::mount_modules_systemlessly(&metadata_dir, &content_dir)?; + + info!("Mount completed successfully"); + Ok(()) +} diff --git a/userspace/meta-overlayfs/src/mount.rs b/userspace/meta-overlayfs/src/mount.rs new file mode 100644 index 00000000..07fd92f6 --- /dev/null +++ b/userspace/meta-overlayfs/src/mount.rs @@ -0,0 +1,376 @@ +// Overlayfs mounting implementation +// Migrated from ksud/src/mount.rs and ksud/src/init_event.rs + +use anyhow::{Context, Result, bail}; +use log::{info, warn}; +use std::collections::HashMap; +use std::path::{Path, PathBuf}; + +#[cfg(any(target_os = "linux", target_os = "android"))] +use procfs::process::Process; +#[cfg(any(target_os = "linux", target_os = "android"))] +use rustix::{fd::AsFd, fs::CWD, mount::*}; + +use crate::defs::{DISABLE_FILE_NAME, KSU_OVERLAY_SOURCE, SKIP_MOUNT_FILE_NAME, SYSTEM_RW_DIR}; + +#[cfg(any(target_os = "linux", target_os = "android"))] +pub fn mount_overlayfs( + lower_dirs: &[String], + lowest: &str, + upperdir: Option, + workdir: Option, + dest: impl AsRef, +) -> Result<()> { + let lowerdir_config = lower_dirs + .iter() + .map(|s| s.as_ref()) + .chain(std::iter::once(lowest)) + .collect::>() + .join(":"); + info!( + "mount overlayfs on {:?}, lowerdir={}, upperdir={:?}, workdir={:?}", + dest.as_ref(), + lowerdir_config, + upperdir, + workdir + ); + + let upperdir = upperdir + .filter(|up| up.exists()) + .map(|e| e.display().to_string()); + let workdir = workdir + .filter(|wd| wd.exists()) + .map(|e| e.display().to_string()); + + let result = (|| { + let fs = fsopen("overlay", FsOpenFlags::FSOPEN_CLOEXEC)?; + let fs = fs.as_fd(); + fsconfig_set_string(fs, "lowerdir", &lowerdir_config)?; + if let (Some(upperdir), Some(workdir)) = (&upperdir, &workdir) { + fsconfig_set_string(fs, "upperdir", upperdir)?; + fsconfig_set_string(fs, "workdir", workdir)?; + } + fsconfig_set_string(fs, "source", KSU_OVERLAY_SOURCE)?; + fsconfig_create(fs)?; + let mount = fsmount(fs, FsMountFlags::FSMOUNT_CLOEXEC, MountAttrFlags::empty())?; + move_mount( + mount.as_fd(), + "", + CWD, + dest.as_ref(), + MoveMountFlags::MOVE_MOUNT_F_EMPTY_PATH, + ) + })(); + + if let Err(e) = result { + warn!("fsopen mount failed: {e:#}, fallback to mount"); + let mut data = format!("lowerdir={lowerdir_config}"); + if let (Some(upperdir), Some(workdir)) = (upperdir, workdir) { + data = format!("{data},upperdir={upperdir},workdir={workdir}"); + } + mount( + KSU_OVERLAY_SOURCE, + dest.as_ref(), + "overlay", + MountFlags::empty(), + data, + )?; + } + Ok(()) +} + +#[cfg(any(target_os = "linux", target_os = "android"))] +pub fn bind_mount(from: impl AsRef, to: impl AsRef) -> Result<()> { + info!( + "bind mount {} -> {}", + from.as_ref().display(), + to.as_ref().display() + ); + let tree = open_tree( + CWD, + from.as_ref(), + OpenTreeFlags::OPEN_TREE_CLOEXEC + | OpenTreeFlags::OPEN_TREE_CLONE + | OpenTreeFlags::AT_RECURSIVE, + )?; + move_mount( + tree.as_fd(), + "", + CWD, + to.as_ref(), + MoveMountFlags::MOVE_MOUNT_F_EMPTY_PATH, + )?; + Ok(()) +} + +#[cfg(any(target_os = "linux", target_os = "android"))] +fn mount_overlay_child( + mount_point: &str, + relative: &String, + module_roots: &Vec, + stock_root: &String, +) -> Result<()> { + if !module_roots + .iter() + .any(|lower| Path::new(&format!("{lower}{relative}")).exists()) + { + return bind_mount(stock_root, mount_point); + } + if !Path::new(&stock_root).is_dir() { + return Ok(()); + } + let mut lower_dirs: Vec = vec![]; + for lower in module_roots { + let lower_dir = format!("{lower}{relative}"); + let path = Path::new(&lower_dir); + if path.is_dir() { + lower_dirs.push(lower_dir); + } else if path.exists() { + // stock root has been blocked by this file + return Ok(()); + } + } + if lower_dirs.is_empty() { + return Ok(()); + } + // merge modules and stock + if let Err(e) = mount_overlayfs(&lower_dirs, stock_root, None, None, mount_point) { + warn!("failed: {e:#}, fallback to bind mount"); + bind_mount(stock_root, mount_point)?; + } + Ok(()) +} + +#[cfg(any(target_os = "linux", target_os = "android"))] +pub fn mount_overlay( + root: &String, + module_roots: &Vec, + workdir: Option, + upperdir: Option, +) -> Result<()> { + info!("mount overlay for {root}"); + std::env::set_current_dir(root).with_context(|| format!("failed to chdir to {root}"))?; + let stock_root = "."; + + // collect child mounts before mounting the root + let mounts = Process::myself()? + .mountinfo() + .with_context(|| "get mountinfo")?; + let mut mount_seq = mounts + .0 + .iter() + .filter(|m| { + m.mount_point.starts_with(root) && !Path::new(&root).starts_with(&m.mount_point) + }) + .map(|m| m.mount_point.to_str()) + .collect::>(); + mount_seq.sort(); + mount_seq.dedup(); + + mount_overlayfs(module_roots, root, upperdir, workdir, root) + .with_context(|| "mount overlayfs for root failed")?; + for mount_point in mount_seq.iter() { + let Some(mount_point) = mount_point else { + continue; + }; + let relative = mount_point.replacen(root, "", 1); + let stock_root: String = format!("{stock_root}{relative}"); + if !Path::new(&stock_root).exists() { + continue; + } + if let Err(e) = mount_overlay_child(mount_point, &relative, module_roots, &stock_root) { + warn!("failed to mount overlay for child {mount_point}: {e:#}, revert"); + umount_dir(root).with_context(|| format!("failed to revert {root}"))?; + bail!(e); + } + } + Ok(()) +} + +#[cfg(any(target_os = "linux", target_os = "android"))] +pub fn umount_dir(src: impl AsRef) -> Result<()> { + unmount(src.as_ref(), UnmountFlags::empty()) + .with_context(|| format!("Failed to umount {}", src.as_ref().display()))?; + Ok(()) +} + +#[cfg(not(any(target_os = "linux", target_os = "android")))] +pub fn mount_overlay( + _root: &String, + _module_roots: &Vec, + _workdir: Option, + _upperdir: Option, +) -> Result<()> { + unimplemented!("mount_overlay is only supported on Linux/Android") +} + +#[cfg(not(any(target_os = "linux", target_os = "android")))] +pub fn mount_overlayfs( + _lower_dirs: &[String], + _lowest: &str, + _upperdir: Option, + _workdir: Option, + _dest: impl AsRef, +) -> Result<()> { + unimplemented!("mount_overlayfs is only supported on Linux/Android") +} + +#[cfg(not(any(target_os = "linux", target_os = "android")))] +pub fn bind_mount(_from: impl AsRef, _to: impl AsRef) -> Result<()> { + unimplemented!("bind_mount is only supported on Linux/Android") +} + +// ========== Mount coordination logic (from init_event.rs) ========== + +#[cfg(any(target_os = "linux", target_os = "android"))] +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(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_overlay(&partition, lowerdir, workdir, upperdir) +} + +/// Collect enabled module IDs from metadata directory +/// +/// Reads module list and status from metadata directory, returns enabled module IDs +#[cfg(any(target_os = "linux", target_os = "android"))] +fn collect_enabled_modules(metadata_dir: &str) -> Result> { + let dir = std::fs::read_dir(metadata_dir) + .with_context(|| format!("Failed to read metadata directory: {}", metadata_dir))?; + + let mut enabled = Vec::new(); + + for entry in dir.flatten() { + let path = entry.path(); + if !path.is_dir() { + continue; + } + + let module_id = match entry.file_name().to_str() { + Some(id) => id.to_string(), + None => continue, + }; + + // Check status markers + if path.join(DISABLE_FILE_NAME).exists() { + info!("Module {} is disabled, skipping", module_id); + continue; + } + + if path.join(SKIP_MOUNT_FILE_NAME).exists() { + info!("Module {} has skip_mount, skipping", module_id); + continue; + } + + // Optional: verify module.prop exists + if !path.join("module.prop").exists() { + warn!("Module {} has no module.prop, skipping", module_id); + continue; + } + + info!("Module {} enabled", module_id); + enabled.push(module_id); + } + + Ok(enabled) +} + +/// Dual-directory version of mount_modules_systemlessly +/// +/// Parameters: +/// - metadata_dir: Metadata directory, stores module.prop, disable, skip_mount, etc. +/// - content_dir: Content directory, stores system/, vendor/ and other partition content (ext4 image mount point) +#[cfg(any(target_os = "linux", target_os = "android"))] +pub fn mount_modules_systemlessly(metadata_dir: &str, content_dir: &str) -> Result<()> { + info!("Scanning modules (dual-directory mode)"); + info!(" Metadata: {}", metadata_dir); + info!(" Content: {}", content_dir); + + // 1. Traverse metadata directory, collect enabled module IDs + let enabled_modules = collect_enabled_modules(metadata_dir)?; + + if enabled_modules.is_empty() { + info!("No enabled modules found"); + return Ok(()); + } + + info!("Found {} enabled module(s)", enabled_modules.len()); + + // 2. Initialize partition lowerdir lists + let partition = vec!["vendor", "product", "system_ext", "odm", "oem"]; + let mut system_lowerdir: Vec = Vec::new(); + let mut partition_lowerdir: HashMap> = HashMap::new(); + + for part in &partition { + partition_lowerdir.insert((*part).to_string(), Vec::new()); + } + + // 3. Read module content from content directory + for module_id in &enabled_modules { + let module_content_path = Path::new(content_dir).join(module_id); + + if !module_content_path.exists() { + warn!("Module {} has no content directory, skipping", module_id); + continue; + } + + info!("Processing module: {}", module_id); + + // Collect system partition + let system_path = module_content_path.join("system"); + if system_path.is_dir() { + system_lowerdir.push(system_path.display().to_string()); + info!(" + system/"); + } + + // Collect other partitions + for part in &partition { + let part_path = module_content_path.join(part); + if part_path.is_dir() + && let Some(v) = partition_lowerdir.get_mut(*part) + { + v.push(part_path.display().to_string()); + info!(" + {}/", part); + } + } + } + + // 4. Mount partitions + info!("Mounting partitions..."); + + if let Err(e) = mount_partition("system", &system_lowerdir) { + warn!("mount system failed: {e:#}"); + } + + for (k, v) in partition_lowerdir { + if let Err(e) = mount_partition(&k, &v) { + warn!("mount {k} failed: {e:#}"); + } + } + + info!("All partitions processed"); + Ok(()) +} + +#[cfg(not(any(target_os = "linux", target_os = "android")))] +pub fn mount_modules_systemlessly(_metadata_dir: &str, _content_dir: &str) -> Result<()> { + unimplemented!("mount_modules_systemlessly is only supported on Linux/Android") +}