support metamodule, remove built-in overlayfs mount

Co-authored-by: weishu <twsxtd@gmail.com>
Co-authored-by: YuKongA <70465933+YuKongA@users.noreply.github.com>
Co-authored-by: Ylarod <me@ylarod.cn>
This commit is contained in:
ShirkNeko
2025-11-20 20:13:08 +08:00
parent 8250c0ecc2
commit 954ecd9644
29 changed files with 1757 additions and 877 deletions

Binary file not shown.

Binary file not shown.

View File

@@ -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)]
@@ -189,7 +184,7 @@ enum Debug {
/// Set the manager app, kernel CONFIG_KSU_DEBUG should be enabled.
SetManager {
/// manager package name
#[arg(default_value_t = String::from("me.weishu.kernelsu"))]
#[arg(default_value_t = String::from("com.sukisu.ultra"))]
apk: String,
},
@@ -209,8 +204,6 @@ enum Debug {
/// Get kernel version
Version,
Mount,
/// For testing
Test,
@@ -277,14 +270,14 @@ enum Module {
zip: String,
},
/// Uninstall module <id>
Uninstall {
/// Undo module uninstall mark <id>
UndoUninstall {
/// module id
id: String,
},
/// Restore module <id>
Restore {
/// Uninstall module <id>
Uninstall {
/// module id
id: String,
},
@@ -466,7 +459,7 @@ enum Umount {
/// List all umount paths
List,
/// Clear all custom paths (keep defaults)
/// Clear all recorded umount paths
ClearCustom,
/// Save configuration to file
@@ -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
}

View File

@@ -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");

View File

@@ -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::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}");
}

View File

@@ -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

View File

@@ -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<Self> {
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<String, Node>,
// the module that owned this node
module_path: Option<PathBuf>,
replace: bool,
skip: bool,
}
impl Node {
fn collect_module_files<P>(&mut self, module_dir: P) -> Result<bool>
where
P: AsRef<Path>,
{
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<T>(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<T>(name: T, entry: &DirEntry) -> Option<Self>
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<Option<Node>> {
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<P>(src: P, dst: P) -> Result<()>
where
P: AsRef<Path>,
{
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<P>(path: P, work_dir_path: P, entry: &DirEntry) -> Result<()>
where
P: AsRef<Path>,
{
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<P, WP>(path: P, work_dir_path: WP, current: Node, has_tmpfs: bool) -> Result<()>
where
P: AsRef<Path>,
WP: AsRef<Path>,
{
let mut current = current;
let path = path.as_ref().join(&current.name);
let work_dir_path = work_dir_path.as_ref().join(&current.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) = &current.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) = &current.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) = &current.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(())
}
}

View File

@@ -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;

View File

@@ -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<String, String>) -> 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<PathBuf> {
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<String> {
// 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<PathBuf> {
// 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(())
}

View File

@@ -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, canonicalize, 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<T: AsRef<Path>>(path: T, wait: bool) -> Result<()> {
pub fn exec_script<T: AsRef<Path>>(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<T: AsRef<Path>>(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(|_| ())
@@ -194,7 +174,17 @@ fn exec_script<T: AsRef<Path>>(path: T, wait: bool) -> Result<()> {
}
pub fn exec_stage_script(stage: &str, block: bool) -> Result<()> {
let metamodule_dir = metamodule::get_metamodule_path().and_then(|path| canonicalize(path).ok());
foreach_active_module(|module| {
if metamodule_dir.as_ref().is_some_and(|meta_dir| {
canonicalize(module)
.map(|resolved| resolved == *meta_dir)
.unwrap_or(false)
}) {
return Ok(());
}
let script_path = module.join(format!("{stage}.sh"));
if !script_path.exists() {
return Ok(());
@@ -251,45 +241,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 +331,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<u8> = 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<u8> = 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/<id> 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 +515,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 +546,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 +602,7 @@ fn _list_modules(path: &str) -> Vec<HashMap<String, String>> {
if !path.join("module.prop").exists() {
continue;
}
let mut module_prop_map = match read_module_prop(&path) {
Ok(prop) => prop,
Err(e) => {
@@ -481,26 +612,33 @@ fn _list_modules(path: &str) -> Vec<HashMap<String, String>> {
};
// 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);
}

View File

@@ -62,11 +62,11 @@ pub fn restore_syscon<P: AsRef<Path>>(dir: P) -> Result<()> {
Ok(())
}
fn restore_modules_con<P: AsRef<Path>>(dir: P) -> Result<()> {
fn restore_syscon_if_unlabeled<P: AsRef<Path>>(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<P: AsRef<Path>>(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(())
}

View File

@@ -25,7 +25,6 @@ pub struct UmountConfig {
pub struct UmountManager {
config: UmountConfig,
config_path: PathBuf,
defaults: Vec<UmountEntry>,
}
#[repr(C)]
@@ -65,7 +64,6 @@ impl UmountManager {
Ok(UmountManager {
config,
config_path: path,
defaults: Vec::new(),
})
}
@@ -110,21 +108,15 @@ impl UmountManager {
}
pub fn add_entry(&mut self, path: &str, flags: i32) -> Result<()> {
let exists = self
.defaults
.iter()
.chain(&self.config.entries)
.any(|e| e.path == path);
let exists = self.config.entries.iter().any(|e| e.path == path);
if exists {
return Err(anyhow!("Entry already exists: {}", path));
}
let is_default = Self::get_default_paths().iter().any(|e| e.path == path);
let entry = UmountEntry {
path: path.to_string(),
flags,
is_default,
is_default: false,
};
self.config.entries.push(entry);
@@ -132,24 +124,17 @@ impl UmountManager {
}
pub fn remove_entry(&mut self, path: &str) -> Result<()> {
let entry = self.config.entries.iter().find(|e| e.path == path);
let before = self.config.entries.len();
self.config.entries.retain(|e| e.path != path);
if let Some(entry) = entry {
if entry.is_default {
return Err(anyhow!("Cannot remove default entry: {}", path));
}
} else {
if before == self.config.entries.len() {
return Err(anyhow!("Entry not found: {}", path));
}
self.config.entries.retain(|e| e.path != path);
Ok(())
}
pub fn list_entries(&self) -> Vec<UmountEntry> {
let mut all = self.defaults.clone();
all.extend(self.config.entries.iter().cloned());
all
self.config.entries.clone()
}
pub fn clear_custom_entries(&mut self) -> Result<()> {
@@ -157,55 +142,7 @@ impl UmountManager {
Ok(())
}
pub fn get_default_paths() -> Vec<UmountEntry> {
vec![
UmountEntry {
path: "/odm".to_string(),
flags: 0,
is_default: true,
},
UmountEntry {
path: "/system".to_string(),
flags: 0,
is_default: true,
},
UmountEntry {
path: "/vendor".to_string(),
flags: 0,
is_default: true,
},
UmountEntry {
path: "/product".to_string(),
flags: 0,
is_default: true,
},
UmountEntry {
path: "/system_ext".to_string(),
flags: 0,
is_default: true,
},
UmountEntry {
path: "/data/adb/modules".to_string(),
flags: -1, // MNT_DETACH
is_default: true,
},
UmountEntry {
path: "/debug_ramdisk".to_string(),
flags: -1, // MNT_DETACH
is_default: true,
},
]
}
pub fn init_defaults(&mut self) -> Result<()> {
self.defaults = Self::get_default_paths();
Ok(())
}
pub fn apply_to_kernel(&self) -> Result<()> {
for entry in &self.defaults {
let _ = Self::kernel_add_entry(entry);
}
for entry in &self.config.entries {
Self::kernel_add_entry(entry)?;
}
@@ -234,7 +171,6 @@ impl UmountManager {
pub fn init_umount_manager() -> Result<UmountManager> {
let mut manager = UmountManager::new(None)?;
manager.init_defaults()?;
if !Path::new(CONFIG_FILE).exists() {
manager.save_config()?;

View File

@@ -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<Path>) -> 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<Path>) -> Result<()> {
pub fn ensure_file_exists<T: AsRef<Path>>(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);

4
userspace/meta-overlayfs/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
/target
/out
Cargo.lock
*.log

View File

@@ -0,0 +1,24 @@
[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 }
hole-punch = { git = "https://github.com/tiann/hole-punch" }
[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

View File

@@ -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

View File

@@ -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'"

View File

@@ -0,0 +1,67 @@
#!/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"
IMG_SIZE_MB=2048
EXISTING_IMG="/data/adb/modules/$MODID/modules.img"
if [ -f "$EXISTING_IMG" ]; then
ui_print "- Reusing modules image from previous install"
"$MODPATH/meta-overlayfs" xcp "$EXISTING_IMG" "$IMG_FILE" || \
abort "! Failed to copy existing modules image"
else
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)"
fi
ui_print "- Installation complete"

View File

@@ -0,0 +1,99 @@
#!/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
}
# Determine whether this module should be moved into the ext4 image.
# We only relocate payloads that expose system/ overlays and do not opt out via skip_mount.
module_requires_overlay_move() {
if [ -f "$MODPATH/skip_mount" ]; then
ui_print "- skip_mount flag detected; keeping files under /data/adb/modules"
return 1
fi
if [ ! -d "$MODPATH/system" ]; then
ui_print "- No system/ directory detected; keeping files under /data/adb/modules"
return 1
fi
return 0
}
# Copy SELinux contexts from src tree to destination by mirroring each entry.
copy_selinux_contexts() {
command -v chcon >/dev/null 2>&1 || return 0
SRC="$1"
DST="$2"
if [ -z "$SRC" ] || [ -z "$DST" ] || [ ! -e "$SRC" ] || [ ! -e "$DST" ]; then
return 0
fi
chcon --reference="$SRC" "$DST" 2>/dev/null || true
find "$SRC" -print | while IFS= read -r PATH_SRC; do
if [ "$PATH_SRC" = "$SRC" ]; then
continue
fi
REL_PATH="${PATH_SRC#"${SRC}/"}"
PATH_DST="$DST/$REL_PATH"
if [ -e "$PATH_DST" ] || [ -L "$PATH_DST" ]; then
chcon --reference="$PATH_SRC" "$PATH_DST" 2>/dev/null || true
fi
done
}
# Post-installation: move partition directories to ext4 image
post_install_to_image() {
ui_print "- Copying module content to image"
set_perm_recursive "$MNT_DIR" 0 0 0755 0644
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 "- Copying $partition/"
cp -af "$MODPATH/$partition" "$MOD_IMG_DIR/" || {
ui_print "! Warning: Failed to move $partition"
continue
}
copy_selinux_contexts "$MODPATH/$partition" "$MOD_IMG_DIR/$partition"
fi
done
}
ui_print "- Using meta-overlayfs metainstall"
install_module
if module_requires_overlay_move; then
ensure_image_mounted
post_install_to_image
else
ui_print "- Skipping move to modules image"
fi
ui_print "- Installation complete"

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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";

View File

@@ -0,0 +1,35 @@
use anyhow::Result;
use log::info;
mod defs;
mod mount;
mod xcp;
fn main() -> Result<()> {
let args: Vec<String> = std::env::args().collect();
if matches!(args.get(1), Some(cmd) if cmd == "xcp") {
return xcp::run(&args[2..]);
}
// 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(())
}

View File

@@ -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<PathBuf>,
workdir: Option<PathBuf>,
dest: impl AsRef<Path>,
) -> Result<()> {
let lowerdir_config = lower_dirs
.iter()
.map(|s| s.as_ref())
.chain(std::iter::once(lowest))
.collect::<Vec<_>>()
.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<Path>, to: impl AsRef<Path>) -> 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<String>,
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<String> = 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<String>,
workdir: Option<PathBuf>,
upperdir: Option<PathBuf>,
) -> 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::<Vec<_>>();
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<Path>) -> 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<String>,
_workdir: Option<PathBuf>,
_upperdir: Option<PathBuf>,
) -> 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<PathBuf>,
_workdir: Option<PathBuf>,
_dest: impl AsRef<Path>,
) -> 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<Path>, _to: impl AsRef<Path>) -> 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<String>) -> 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<Vec<String>> {
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<String> = Vec::new();
let mut partition_lowerdir: HashMap<String, Vec<String>> = 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")
}

View File

@@ -0,0 +1,90 @@
use anyhow::{bail, Context, Result};
use hole_punch::*;
use std::{
fs::{File, OpenOptions},
io::{Read, Seek, SeekFrom, Write},
path::Path,
};
/// Handle the `xcp` command: copy sparse file with optional hole punching.
pub fn run(args: &[String]) -> Result<()> {
let mut positional: Vec<&str> = Vec::with_capacity(2);
let mut punch_hole = false;
for arg in args {
match arg.as_str() {
"--punch-hole" => punch_hole = true,
"-h" | "--help" => {
print_usage();
return Ok(());
}
_ => positional.push(arg),
}
}
if positional.len() < 2 {
print_usage();
bail!("xcp requires source and destination paths");
}
if positional.len() > 2 {
bail!("unexpected argument: {}", positional[2]);
}
copy_sparse_file(positional[0], positional[1], punch_hole)
}
fn print_usage() {
eprintln!("Usage: meta-overlayfs xcp <src> <dst> [--punch-hole]");
}
// TODO: use libxcp to improve the speed if cross's MSRV is 1.70
pub fn copy_sparse_file<P: AsRef<Path>, Q: AsRef<Path>>(
src: P,
dst: Q,
punch_hole: bool,
) -> Result<()> {
let mut src_file = File::open(src.as_ref())
.with_context(|| format!("failed to open {}", src.as_ref().display()))?;
let mut dst_file = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(dst.as_ref())
.with_context(|| format!("failed to open {}", dst.as_ref().display()))?;
dst_file.set_len(src_file.metadata()?.len())?;
let segments = src_file.scan_chunks()?;
for segment in segments {
if let SegmentType::Data = segment.segment_type {
let start = segment.start;
let end = segment.end + 1;
src_file.seek(SeekFrom::Start(start))?;
dst_file.seek(SeekFrom::Start(start))?;
let mut buffer = [0; 4096];
let mut total_bytes_copied = 0;
while total_bytes_copied < end - start {
let bytes_to_read =
std::cmp::min(buffer.len() as u64, end - start - total_bytes_copied);
let bytes_read = src_file.read(&mut buffer[..bytes_to_read as usize])?;
if bytes_read == 0 {
break;
}
if punch_hole && buffer[..bytes_read].iter().all(|&x| x == 0) {
dst_file.seek(SeekFrom::Current(bytes_read as i64))?;
total_bytes_copied += bytes_read as u64;
continue;
}
dst_file.write_all(&buffer[..bytes_read])?;
total_bytes_copied += bytes_read as u64;
}
}
}
Ok(())
}