From a8acea91807fde211239cef615445671b4072e24 Mon Sep 17 00:00:00 2001 From: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com> Date: Wed, 19 Nov 2025 19:33:01 +0800 Subject: [PATCH] support metamodule, remove built-in overlayfs mount Co-authored-by: weishu Co-authored-by: YuKongA <70465933+YuKongA@users.noreply.github.com> Co-authored-by: Ylarod --- .github/workflows/build-manager.yml | 5 +- .github/workflows/meta-overlay.yml | 54 ++ userspace/ksud/bin/aarch64/resetprop | Bin 75368 -> 74720 bytes userspace/ksud/bin/x86_64/resetprop | Bin 76104 -> 75688 bytes userspace/ksud/src/cli.rs | 32 +- userspace/ksud/src/defs.rs | 15 +- userspace/ksud/src/init_event.rs | 88 ++- userspace/ksud/src/installer.sh | 39 +- userspace/ksud/src/magic_mount.rs | 465 ---------------- userspace/ksud/src/main.rs | 4 +- userspace/ksud/src/metamodule.rs | 287 ++++++++++ userspace/ksud/src/module.rs | 518 +++++++++++------- userspace/ksud/src/restorecon.rs | 6 +- userspace/ksud/src/utils.rs | 42 +- userspace/meta-overlayfs/.gitignore | 4 + userspace/meta-overlayfs/Cargo.toml | 23 + userspace/meta-overlayfs/README.md | 58 ++ userspace/meta-overlayfs/build.sh | 92 ++++ userspace/meta-overlayfs/metamodule/.gitkeep | 0 .../meta-overlayfs/metamodule/customize.sh | 77 +++ .../meta-overlayfs/metamodule/metainstall.sh | 61 +++ .../meta-overlayfs/metamodule/metamount.sh | 65 +++ .../metamodule/metauninstall.sh | 35 ++ .../meta-overlayfs/metamodule/module.prop | 8 + .../meta-overlayfs/metamodule/uninstall.sh | 24 + userspace/meta-overlayfs/src/defs.rs | 17 + userspace/meta-overlayfs/src/main.rs | 29 + userspace/meta-overlayfs/src/mount.rs | 376 +++++++++++++ 28 files changed, 1617 insertions(+), 807 deletions(-) create mode 100644 .github/workflows/meta-overlay.yml delete mode 100644 userspace/ksud/src/magic_mount.rs create mode 100644 userspace/ksud/src/metamodule.rs create mode 100644 userspace/meta-overlayfs/.gitignore create mode 100644 userspace/meta-overlayfs/Cargo.toml create mode 100644 userspace/meta-overlayfs/README.md create mode 100644 userspace/meta-overlayfs/build.sh create mode 100644 userspace/meta-overlayfs/metamodule/.gitkeep create mode 100644 userspace/meta-overlayfs/metamodule/customize.sh create mode 100644 userspace/meta-overlayfs/metamodule/metainstall.sh create mode 100644 userspace/meta-overlayfs/metamodule/metamount.sh create mode 100644 userspace/meta-overlayfs/metamodule/metauninstall.sh create mode 100644 userspace/meta-overlayfs/metamodule/module.prop create mode 100644 userspace/meta-overlayfs/metamodule/uninstall.sh create mode 100644 userspace/meta-overlayfs/src/defs.rs create mode 100644 userspace/meta-overlayfs/src/main.rs create mode 100644 userspace/meta-overlayfs/src/mount.rs 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 2dc7d0a400997d98e24389572a67187bd8935d9f..dd58ca45deae0c2c0e9704d46d8c63adb061c473 100644 GIT binary patch delta 18872 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 Y%P;UZ;GZhe1lEVS^Y5{a31)`>13uBA_W%F@ delta 19715 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 diff --git a/userspace/ksud/bin/x86_64/resetprop b/userspace/ksud/bin/x86_64/resetprop index 80030612649bbf17fea4052597e5c7233e85a363..9048971a425cb4c07f5bb9a620c5a274445b248e 100644 GIT binary patch literal 75688 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
cf0HPoFWo=V0{7_&YFVW!` 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 literal 76104 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 %68`zJ_TF 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 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") +}