commit
156b452dae
13 changed files with 2138 additions and 0 deletions
3
README.md
Normal file
3
README.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
## Magic Mount Metamodule
|
||||
|
||||
Based on https://github.com/5ec1cff/KernelSU/blob/52f1f575ce2bd0ca46ebf644fd00a838af9f344e/userspace/ksud/src/magic_mount.rs
|
||||
2
magic_mount/.gitignore
vendored
Normal file
2
magic_mount/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
/target
|
||||
.cargo/
|
||||
19
magic_mount/Cargo.toml
Normal file
19
magic_mount/Cargo.toml
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
[package]
|
||||
name = "magic_mount_rs"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
extattr = "1"
|
||||
log = "0.4"
|
||||
simplelog = "0.12"
|
||||
rustix = { version = "0.38", features = ["fs", "mount"] }
|
||||
libc = "0.2"
|
||||
|
||||
[profile.release]
|
||||
strip = true
|
||||
opt-level = "z"
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
489
magic_mount/src/magic_mount.rs
Normal file
489
magic_mount/src/magic_mount.rs
Normal file
|
|
@ -0,0 +1,489 @@
|
|||
use crate::magic_mount::NodeFileType::{Directory, RegularFile, Symlink, Whiteout};
|
||||
use crate::utils::{lgetfilecon, lsetfilecon, ensure_dir_exists};
|
||||
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,
|
||||
};
|
||||
use rustix::mount::mount_change;
|
||||
use rustix::path::Arg;
|
||||
use std::cmp::PartialEq;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::fs;
|
||||
use std::fs::{DirEntry, FileType, create_dir, create_dir_all, read_dir, read_link};
|
||||
use std::os::unix::fs::{FileTypeExt, symlink};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
const DISABLE_FILE_NAME: &str = "disable";
|
||||
const REMOVE_FILE_NAME: &str = "remove";
|
||||
const SKIP_MOUNT_FILE_NAME: &str = "skip_mount";
|
||||
|
||||
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<T: AsRef<Path>>(&mut self, module_dir: T) -> Result<bool> {
|
||||
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: ToString>(name: T) -> Self {
|
||||
Node {
|
||||
name: name.to_string(),
|
||||
file_type: Directory,
|
||||
children: Default::default(),
|
||||
module_path: None,
|
||||
replace: false,
|
||||
skip: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn new_module<T: ToString>(name: T, entry: &DirEntry) -> Option<Self> {
|
||||
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(module_dir: &Path, extra_partitions: &[String]) -> Result<Option<Node>> {
|
||||
let mut root = Node::new_root("");
|
||||
let mut system = Node::new_root("system");
|
||||
let module_root = 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(REMOVE_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 {
|
||||
const BUILTIN_PARTITIONS: [(&str, bool); 4] = [
|
||||
("vendor", true),
|
||||
("system_ext", true),
|
||||
("product", true),
|
||||
("odm", false),
|
||||
];
|
||||
|
||||
for (partition, require_symlink) in BUILTIN_PARTITIONS {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for partition in extra_partitions {
|
||||
if BUILTIN_PARTITIONS.iter().any(|(p, _)| p == partition) {
|
||||
continue;
|
||||
}
|
||||
if partition == "system" {
|
||||
continue;
|
||||
}
|
||||
|
||||
let path_of_root = Path::new("/").join(partition);
|
||||
let path_of_system = Path::new("/system").join(partition);
|
||||
let require_symlink = false;
|
||||
|
||||
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) {
|
||||
log::debug!("attach extra partition '{}' to root", name);
|
||||
root.children.insert(name, node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
root.children.insert("system".to_string(), system);
|
||||
Ok(Some(root))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
fn clone_symlink<Src: AsRef<Path>, Dst: AsRef<Path>>(src: Src, dst: Dst) -> Result<()> {
|
||||
let src_symlink = read_link(src.as_ref())?;
|
||||
symlink(&src_symlink, dst.as_ref())?;
|
||||
lsetfilecon(dst.as_ref(), lgetfilecon(src.as_ref())?.as_str())?;
|
||||
log::debug!(
|
||||
"clone symlink {} -> {}({})",
|
||||
dst.as_ref().display(),
|
||||
dst.as_ref().display(),
|
||||
src_symlink.display()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn mount_mirror<P: AsRef<Path>, WP: AsRef<Path>>(
|
||||
path: P,
|
||||
work_dir_path: WP,
|
||||
entry: &DirEntry,
|
||||
) -> Result<()> {
|
||||
let path = path.as_ref().join(entry.file_name());
|
||||
let work_dir_path = work_dir_path.as_ref().join(entry.file_name());
|
||||
let file_type = entry.file_type()?;
|
||||
|
||||
if file_type.is_file() {
|
||||
log::debug!(
|
||||
"mount mirror file {} -> {}",
|
||||
path.display(),
|
||||
work_dir_path.display()
|
||||
);
|
||||
fs::File::create(&work_dir_path)?;
|
||||
bind_mount(&path, &work_dir_path)?;
|
||||
} else if file_type.is_dir() {
|
||||
log::debug!(
|
||||
"mount mirror dir {} -> {}",
|
||||
path.display(),
|
||||
work_dir_path.display()
|
||||
);
|
||||
create_dir(&work_dir_path)?;
|
||||
let metadata = entry.metadata()?;
|
||||
chmod(&work_dir_path, Mode::from_raw_mode(metadata.mode()))?;
|
||||
unsafe {
|
||||
chown(
|
||||
&work_dir_path,
|
||||
Some(Uid::from_raw(metadata.uid())),
|
||||
Some(Gid::from_raw(metadata.gid())),
|
||||
)?;
|
||||
}
|
||||
lsetfilecon(&work_dir_path, lgetfilecon(&path)?.as_str())?;
|
||||
for entry in read_dir(&path)?.flatten() {
|
||||
mount_mirror(&path, &work_dir_path, &entry)?;
|
||||
}
|
||||
} else if file_type.is_symlink() {
|
||||
log::debug!(
|
||||
"create mirror symlink {} -> {}",
|
||||
path.display(),
|
||||
work_dir_path.display()
|
||||
);
|
||||
clone_symlink(&path, &work_dir_path)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn do_magic_mount<P: AsRef<Path>, WP: AsRef<Path>>(
|
||||
path: P,
|
||||
work_dir_path: WP,
|
||||
current: Node,
|
||||
has_tmpfs: bool,
|
||||
) -> Result<()> {
|
||||
let mut current = current;
|
||||
let path = path.as_ref().join(¤t.name);
|
||||
let work_dir_path = work_dir_path.as_ref().join(¤t.name);
|
||||
match current.file_type {
|
||||
RegularFile => {
|
||||
let target_path = if has_tmpfs {
|
||||
fs::File::create(&work_dir_path)?;
|
||||
&work_dir_path
|
||||
} else {
|
||||
&path
|
||||
};
|
||||
if let Some(module_path) = ¤t.module_path {
|
||||
log::debug!(
|
||||
"mount module file {} -> {}",
|
||||
module_path.display(),
|
||||
work_dir_path.display()
|
||||
);
|
||||
bind_mount(module_path, target_path).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<T: AsRef<Path>>(
|
||||
tmp_path: T,
|
||||
module_dir: &Path,
|
||||
mount_source: &str,
|
||||
extra_partitions: &[String],
|
||||
) -> Result<()> {
|
||||
if let Some(root) = collect_module_files(module_dir, extra_partitions)? {
|
||||
log::debug!("collected: {:#?}", root);
|
||||
|
||||
let tmp_root = tmp_path.as_ref();
|
||||
let tmp_dir = tmp_root.join("workdir");
|
||||
ensure_dir_exists(&tmp_dir)?;
|
||||
|
||||
mount(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(())
|
||||
}
|
||||
}
|
||||
82
magic_mount/src/main.rs
Normal file
82
magic_mount/src/main.rs
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
mod magic_mount;
|
||||
mod utils;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use clap::Parser;
|
||||
use std::path::PathBuf;
|
||||
|
||||
const MODULE_DIR_DEFAULT: &str = "/data/adb/modules/";
|
||||
const MOUNT_SOURCE_DEFAULT: &str = "MaGIcMounT";
|
||||
const LOG_FILE_DEFAULT: &str = "/data/adb/mm.log";
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(name = "magic_mount", version, about = "Magic Mount Metamodule")]
|
||||
struct Cli {
|
||||
/// Module directory path
|
||||
#[arg(short = 'm', long = "moduledir", default_value = MODULE_DIR_DEFAULT)]
|
||||
moduledir: PathBuf,
|
||||
|
||||
/// Temporary directory path (auto-selected if not specified)
|
||||
#[arg(short = 't', long = "tempdir")]
|
||||
tempdir: Option<PathBuf>,
|
||||
|
||||
/// Mount source name
|
||||
#[arg(short = 's', long = "mountsource", default_value = MOUNT_SOURCE_DEFAULT)]
|
||||
mountsource: String,
|
||||
|
||||
/// Log file path
|
||||
#[arg(short = 'l', long = "logfile", default_value = LOG_FILE_DEFAULT)]
|
||||
logfile: PathBuf,
|
||||
|
||||
/// Enable verbose (debug) logging
|
||||
#[arg(short = 'v', long = "verbose")]
|
||||
verbose: bool,
|
||||
|
||||
/// Extra partitions, comma-separated, eg: -p mi_ext,my_stock
|
||||
#[arg(short = 'p', long = "partitions", value_delimiter = ',')]
|
||||
partitions: Vec<String>,
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let cli = Cli::parse();
|
||||
|
||||
utils::init_logger(&cli.logfile, cli.verbose)?;
|
||||
|
||||
log::info!("Magic Mount Starting");
|
||||
log::info!("module dir : {}", cli.moduledir.display());
|
||||
|
||||
let tempdir = if let Some(temp) = cli.tempdir {
|
||||
log::info!("temp dir (user) : {}", temp.display());
|
||||
temp
|
||||
} else {
|
||||
let temp = utils::select_temp_dir()
|
||||
.context("failed to select temp dir automatically")?;
|
||||
log::info!("temp dir (auto) : {}", temp.display());
|
||||
temp
|
||||
};
|
||||
|
||||
log::info!("mount source : {}", cli.mountsource);
|
||||
log::info!("log file : {}", cli.logfile.display());
|
||||
log::info!("verbose mode : {}", cli.verbose);
|
||||
if !cli.partitions.is_empty() {
|
||||
log::info!("extra partitions: {:?}", cli.partitions);
|
||||
}
|
||||
|
||||
utils::ensure_temp_dir(&tempdir)?;
|
||||
|
||||
let result = magic_mount::magic_mount(&tempdir, &cli.moduledir, &cli.mountsource, &cli.partitions);
|
||||
|
||||
utils::cleanup_temp_dir(&tempdir);
|
||||
|
||||
match result {
|
||||
Ok(_) => {
|
||||
log::info!("Magic Mount Completed Successfully");
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Magic Mount Failed");
|
||||
log::error!("error: {:#}", e);
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
159
magic_mount/src/utils.rs
Normal file
159
magic_mount/src/utils.rs
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
use anyhow::{bail, Context, Result};
|
||||
use log::LevelFilter;
|
||||
use simplelog::{Config, WriteLogger};
|
||||
use std::fs::{OpenOptions, write, remove_file, create_dir_all, remove_dir_all, read_to_string};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
use extattr::{lsetxattr, Flags as XattrFlags};
|
||||
|
||||
const SELINUX_XATTR: &str = "security.selinux";
|
||||
|
||||
const TEMP_DIR_SUFFIX: &str = ".magic_mount";
|
||||
|
||||
const TMPFS_CANDIDATES: &[&str] = &["/mnt/vendor", "/mnt", "/debug_ramdisk"];
|
||||
|
||||
pub fn lsetfilecon<P: AsRef<Path>>(path: P, con: &str) -> Result<()> {
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
lsetxattr(&path, SELINUX_XATTR, con, XattrFlags::empty()).with_context(|| {
|
||||
format!(
|
||||
"Failed to change SELinux context for {}",
|
||||
path.as_ref().display()
|
||||
)
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
pub fn lgetfilecon<P: AsRef<Path>>(path: P) -> Result<String> {
|
||||
let con = extattr::lgetxattr(&path, SELINUX_XATTR).with_context(|| {
|
||||
format!(
|
||||
"Failed to get SELinux context for {}",
|
||||
path.as_ref().display()
|
||||
)
|
||||
})?;
|
||||
let con = String::from_utf8_lossy(&con);
|
||||
Ok(con.to_string())
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "linux", target_os = "android")))]
|
||||
pub fn lgetfilecon<P: AsRef<Path>>(path: P) -> Result<String> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
pub fn ensure_dir_exists<T: AsRef<Path>>(dir: T) -> Result<()> {
|
||||
let result = create_dir_all(&dir);
|
||||
if dir.as_ref().is_dir() && result.is_ok() {
|
||||
Ok(())
|
||||
} else {
|
||||
bail!("{} is not a regular directory", dir.as_ref().display())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_logger(logfile: &Path, verbose: bool) -> Result<()> {
|
||||
let level = if verbose {
|
||||
LevelFilter::Debug
|
||||
} else if cfg!(debug_assertions) {
|
||||
LevelFilter::Debug
|
||||
} else {
|
||||
LevelFilter::Info
|
||||
};
|
||||
|
||||
let file = OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(logfile)
|
||||
.with_context(|| format!("failed to open log file {logfile:?}"))?;
|
||||
|
||||
WriteLogger::init(level, Config::default(), file)
|
||||
.context("failed to init file logger")?;
|
||||
|
||||
log::info!("log level: {:?}", level);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_writable_tmpfs(path: &Path) -> bool {
|
||||
if !path.is_dir() {
|
||||
log::debug!(" {} is not a directory", path.display());
|
||||
return false;
|
||||
}
|
||||
|
||||
if let Ok(mounts) = read_to_string("/proc/mounts") {
|
||||
let path_str = path.to_string_lossy();
|
||||
let is_tmpfs = mounts.lines().any(|line| {
|
||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||
parts.len() >= 3 && parts[1] == path_str && parts[2] == "tmpfs"
|
||||
});
|
||||
|
||||
if !is_tmpfs {
|
||||
log::debug!(" {} is not a tmpfs", path.display());
|
||||
return false;
|
||||
}
|
||||
log::debug!(" {} is a tmpfs", path.display());
|
||||
} else {
|
||||
log::debug!(" failed to read /proc/mounts");
|
||||
}
|
||||
|
||||
let test_file = path.join(format!(".mm_test_{}", std::process::id()));
|
||||
let writable = write(&test_file, b"test").is_ok();
|
||||
|
||||
if writable {
|
||||
let _ = remove_file(&test_file);
|
||||
log::debug!(" {} is writable", path.display());
|
||||
} else {
|
||||
log::debug!(" {} is not writable", path.display());
|
||||
}
|
||||
|
||||
writable
|
||||
}
|
||||
|
||||
pub fn select_temp_dir() -> Result<PathBuf> {
|
||||
log::debug!("searching for suitable tmpfs mount point...");
|
||||
|
||||
for candidate in TMPFS_CANDIDATES {
|
||||
let path = Path::new(candidate);
|
||||
log::debug!("checking tmpfs candidate: {}", path.display());
|
||||
|
||||
if is_writable_tmpfs(path) {
|
||||
let temp_dir = path.join(TEMP_DIR_SUFFIX);
|
||||
log::info!(
|
||||
"selected tmpfs: {} -> {}",
|
||||
path.display(),
|
||||
temp_dir.display()
|
||||
);
|
||||
return Ok(temp_dir);
|
||||
}
|
||||
}
|
||||
|
||||
bail!(
|
||||
"no writable tmpfs found in candidates: {}",
|
||||
TMPFS_CANDIDATES.join(", ")
|
||||
)
|
||||
}
|
||||
|
||||
pub fn ensure_temp_dir(temp_dir: &Path) -> Result<()> {
|
||||
if temp_dir.exists() {
|
||||
log::debug!("cleaning existing temp dir: {}", temp_dir.display());
|
||||
remove_dir_all(temp_dir)
|
||||
.with_context(|| format!("failed to clean temp dir {temp_dir:?}"))?;
|
||||
}
|
||||
|
||||
create_dir_all(temp_dir)
|
||||
.with_context(|| format!("failed to create temp dir {temp_dir:?}"))?;
|
||||
|
||||
log::debug!("temp dir ready: {}", temp_dir.display());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn cleanup_temp_dir(temp_dir: &Path) {
|
||||
if let Err(e) = remove_dir_all(temp_dir) {
|
||||
log::warn!(
|
||||
"failed to clean up temp dir {}: {:#}",
|
||||
temp_dir.display(),
|
||||
e
|
||||
);
|
||||
} else {
|
||||
log::debug!("cleaned up temp dir: {}", temp_dir.display());
|
||||
}
|
||||
}
|
||||
43
module/customize.sh
Normal file
43
module/customize.sh
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
#!/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)
|
||||
ui_print "- Selected architecture: ARM64"
|
||||
ARCH_BINARY="mm_arm64"
|
||||
;;
|
||||
armeabi-v7a)
|
||||
ui_print "- Selected architecture: ARM"
|
||||
ARCH_BINARY="mm_arm"
|
||||
;;
|
||||
*)
|
||||
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-mm"
|
||||
|
||||
# Rename the selected binary to the generic name
|
||||
mv "$MODPATH/$ARCH_BINARY" "$MODPATH/meta-mm" || abort "! Failed to rename binary"
|
||||
|
||||
# Remove the unused binary
|
||||
# rm -f "$MODPATH/$REMOVE_BINARY"
|
||||
|
||||
# Ensure the binary is executable
|
||||
chmod 755 "$MODPATH/meta-mm" || abort "! Failed to set permissions"
|
||||
|
||||
ui_print "- Architecture-specific binary installed successfully"
|
||||
|
||||
ui_print "- Installation complete"
|
||||
ui_print "- Image is ready for module installations"
|
||||
1271
module/magic_mount.c
Normal file
1271
module/magic_mount.c
Normal file
File diff suppressed because it is too large
Load diff
9
module/metainstall.sh
Normal file
9
module/metainstall.sh
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
#!/system/bin/sh
|
||||
############################################
|
||||
# meta-mm metainstall.sh
|
||||
############################################
|
||||
|
||||
# Main installation flow
|
||||
ui_print "- Using meta-mm metainstall"
|
||||
|
||||
ui_print "- Installation complete"
|
||||
35
module/metamount.sh
Normal file
35
module/metamount.sh
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
#!/system/bin/sh
|
||||
# meta-overlayfs Module Mount Handler
|
||||
# This script is the entry point for dual-directory module mounting
|
||||
|
||||
MODDIR="${0%/*}"
|
||||
|
||||
# Binary path (architecture-specific binary selected during installation)
|
||||
BINARY="$MODDIR/meta-mm"
|
||||
|
||||
if [ ! -f "$BINARY" ]; then
|
||||
log "ERROR: Binary not found: $BINARY"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Set dual-directory environment variables
|
||||
MODULE_METADATA_DIR="/data/adb/modules"
|
||||
MODULE_SOURCE="KSU"
|
||||
MODULE_METADATA_LOGFILE="/data/adb/mm.log"
|
||||
|
||||
if [ -f "$MODULE_METADATA_LOGFILE" ]; then
|
||||
mv "$MODULE_METADATA_LOGFILE" "$MODULE_METADATA_LOGFILE".bak
|
||||
fi
|
||||
|
||||
# Execute the mount binary
|
||||
"$BINARY" -s "$MODULE_SOURCE" -m "$MODULE_METADATA_DIR" -v -l "$MODULE_METADATA_LOGFILE"
|
||||
|
||||
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
|
||||
6
module/metauninstall.sh
Normal file
6
module/metauninstall.sh
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
#!/system/bin/sh
|
||||
############################################
|
||||
# mm-mm metauninstall.sh
|
||||
############################################
|
||||
|
||||
exit 0
|
||||
8
module/module.prop
Normal file
8
module/module.prop
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
id=meta-mm
|
||||
metamodule=1
|
||||
name=Magic Mount MetaModule
|
||||
version=0.1.0
|
||||
versionCode=2511190013
|
||||
author=7a72
|
||||
description=An implementation of a metamodule using Magic Mount, Based on MKSU, Template is from meta-overlay
|
||||
updateJson=https://zrlab.org/api/m/k/mm/update.json
|
||||
12
module/uninstall.sh
Normal file
12
module/uninstall.sh
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
#!/system/bin/sh
|
||||
############################################
|
||||
# mm-mm uninstall.sh
|
||||
# Cleanup script for metamodule removal
|
||||
############################################
|
||||
|
||||
MODDIR="${0%/*}"
|
||||
|
||||
rm -rf /data/adb/mm.log
|
||||
rm -rf /data/adb/mm.log.bak
|
||||
|
||||
exit 0
|
||||
Loading…
Reference in a new issue