refactor: rewrite in C

Migrate from Rust to C for better compatibility and smaller binaries.
All functionality preserved.
This commit is contained in:
7a72 2025-11-20 19:27:26 +08:00
commit 5b41d03592
21 changed files with 1323 additions and 2030 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

246
build.sh Normal file
View file

@ -0,0 +1,246 @@
#!/bin/bash
# Color definitions
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Default values
VERSION=""
BUILD_TYPES=()
# Show usage
usage() {
echo "Usage: $0 [--release] [--debug]"
echo " --release Build release version"
echo " --debug Build debug version"
echo " (no option) Build both release and debug versions"
echo ""
echo "Version number will be automatically obtained from git tag"
exit 1
}
# Parse command line arguments
while [[ $# -gt 0 ]]; do
case $1 in
--release)
BUILD_TYPES+=("release")
shift
;;
--debug)
BUILD_TYPES+=("debug")
shift
;;
-h|--help)
usage
;;
*)
echo -e "${RED}Error: Unknown parameter $1${NC}"
usage
;;
esac
done
# If no build type specified, build both
if [ ${#BUILD_TYPES[@]} -eq 0 ]; then
BUILD_TYPES=("release" "debug")
echo -e "${YELLOW}No build type specified, building both release and debug versions${NC}"
fi
# Check if in git repository
if ! git rev-parse --git-dir > /dev/null 2>&1; then
echo -e "${RED}Error: Not in a git repository${NC}"
exit 1
fi
# Get git tag as version number
VERSION=$(git describe --tags --exact-match 2>/dev/null)
if [ -z "$VERSION" ]; then
# If current commit has no tag, try to get the nearest tag
VERSION=$(git describe --tags --abbrev=0 2>/dev/null)
if [ -z "$VERSION" ]; then
echo -e "${RED}Error: Unable to get git tag, please create a tag first${NC}"
echo -e "${YELLOW}Hint: Use 'git tag v1.0.0' to create a tag${NC}"
exit 1
else
echo -e "${YELLOW}Warning: Current commit has no tag, using nearest tag: $VERSION${NC}"
fi
fi
# Get git short commit or branch name
GIT_SUFFIX=""
GIT_COMMIT=$(git rev-parse --short HEAD 2>/dev/null)
if [ -n "$GIT_COMMIT" ]; then
GIT_SUFFIX="$GIT_COMMIT"
else
# If unable to get commit, use branch name
GIT_BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
if [ -n "$GIT_BRANCH" ]; then
# Clean branch name, remove special characters
GIT_SUFFIX=$(echo "$GIT_BRANCH" | sed 's/[^a-zA-Z0-9_-]/_/g')
echo -e "${YELLOW}Warning: Unable to get commit, using branch name: $GIT_BRANCH${NC}"
else
echo -e "${YELLOW}Warning: Unable to get commit and branch information${NC}"
fi
fi
# Function to build for a specific type
build_for_type() {
local BUILD_TYPE=$1
echo -e "\n${GREEN}========================================${NC}"
echo -e "${GREEN}Building $BUILD_TYPE version${NC}"
echo -e "${GREEN}Version: $VERSION${NC}"
echo -e "${GREEN}Build Type: $BUILD_TYPE${NC}"
if [ -n "$GIT_COMMIT" ]; then
echo -e "${GREEN}Git Commit: $GIT_COMMIT${NC}"
elif [ -n "$GIT_SUFFIX" ]; then
echo -e "${GREEN}Git Branch: $GIT_SUFFIX${NC}"
fi
echo -e "${GREEN}========================================${NC}"
# Step 1: Enter src directory and execute make
echo -e "\n${YELLOW}[1/5] Compiling source code...${NC}"
if [ ! -d "src" ]; then
echo -e "${RED}Error: src directory does not exist${NC}"
return 1
fi
cd src || return 1
make clean
make $BUILD_TYPE version="$VERSION"
if [ $? -ne 0 ]; then
echo -e "${RED}Error: Compilation failed${NC}"
cd ..
return 1
fi
cd ..
echo -e "${GREEN}Compilation completed${NC}"
# Step 2: Modify log level in metamount.sh
echo -e "\n${YELLOW}[2/5] Modifying metamount.sh configuration...${NC}"
if [ ! -f "template/metamount.sh" ]; then
echo -e "${RED}Error: template/metamount.sh does not exist${NC}"
return 1
fi
if [ "$BUILD_TYPE" = "debug" ]; then
LOG_LEVEL=3
else
LOG_LEVEL=2
fi
# Use sed to modify log level
sed -i "s/MODULE_METADATA_LOGLEVEL=.*/MODULE_METADATA_LOGLEVEL=$LOG_LEVEL/" template/metamount.sh
if [ $? -ne 0 ]; then
echo -e "${RED}Error: Failed to modify metamount.sh${NC}"
return 1
fi
echo -e "${GREEN}metamount.sh configuration completed (LOG_LEVEL=$LOG_LEVEL)${NC}"
# Step 3: Modify module.prop
echo -e "\n${YELLOW}[3/5] Modifying module.prop...${NC}"
if [ ! -f "template/module.prop" ]; then
echo -e "${RED}Error: template/module.prop does not exist${NC}"
return 1
fi
# Generate versionCode (format: yymmddhhmm)
VERSION_CODE=$(date +%y%m%d%H%M)
# Modify version and versionCode
sed -i "s/^version=.*/version=$VERSION/" template/module.prop
sed -i "s/^versionCode=.*/versionCode=$VERSION_CODE/" template/module.prop
if [ $? -ne 0 ]; then
echo -e "${RED}Error: Failed to modify module.prop${NC}"
return 1
fi
echo -e "${GREEN}module.prop configuration completed (version=$VERSION, versionCode=$VERSION_CODE)${NC}"
# Step 4: Move bin directory
echo -e "\n${YELLOW}[4/5] Moving bin directory...${NC}"
if [ ! -d "src/bin" ]; then
echo -e "${RED}Error: src/bin directory does not exist${NC}"
return 1
fi
# Remove target if it exists
if [ -d "template/bin" ]; then
rm -rf template/bin
fi
cp -r src/bin template/
if [ $? -ne 0 ]; then
echo -e "${RED}Error: Failed to copy bin directory${NC}"
return 1
fi
echo -e "${GREEN}bin directory copied${NC}"
# Step 5: Package as zip
echo -e "\n${YELLOW}[5/5] Packaging module...${NC}"
# Build filename
if [ -n "$GIT_SUFFIX" ]; then
OUTPUT_NAME="meta-magic_mount-${VERSION}-${BUILD_TYPE}-${VERSION_CODE}-${GIT_SUFFIX}.zip"
else
OUTPUT_NAME="meta-magic_mount-${VERSION}-${BUILD_TYPE}-${VERSION_CODE}.zip"
fi
mkdir build
# Enter template directory and package
cd template || return 1
zip -r "../build/$OUTPUT_NAME" ./* -x "*.git*"
if [ $? -ne 0 ]; then
echo -e "${RED}Error: Packaging failed${NC}"
cd ..
return 1
fi
cd ..
# Remove target if it exists
if [ -d "template/bin" ]; then
rm -rf template/bin
fi
if [ -d "src/bin" ]; then
rm -rf src/bin
fi
echo -e "\n${GREEN}========================================${NC}"
echo -e "${GREEN}Build completed!${NC}"
echo -e "${GREEN}Output file: $OUTPUT_NAME${NC}"
echo -e "${GREEN}========================================${NC}"
return 0
}
# Build for each specified type
SUCCESS_COUNT=0
FAIL_COUNT=0
for BUILD_TYPE in "${BUILD_TYPES[@]}"; do
build_for_type "$BUILD_TYPE"
if [ $? -eq 0 ]; then
((SUCCESS_COUNT++))
else
((FAIL_COUNT++))
echo -e "${RED}Failed to build $BUILD_TYPE version${NC}"
fi
done
# Print summary
echo -e "\n${GREEN}========================================${NC}"
echo -e "${GREEN}Build Summary${NC}"
echo -e "${GREEN}Total builds: ${#BUILD_TYPES[@]}${NC}"
echo -e "${GREEN}Successful: $SUCCESS_COUNT${NC}"
if [ $FAIL_COUNT -gt 0 ]; then
echo -e "${RED}Failed: $FAIL_COUNT${NC}"
fi
echo -e "${GREEN}========================================${NC}"
# Exit with error if any build failed
if [ $FAIL_COUNT -gt 0 ]; then
exit 1
fi
exit 0

View file

@ -1,2 +0,0 @@
/target
.cargo/

View file

@ -1,19 +0,0 @@
[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

View file

@ -1,489 +0,0 @@
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(&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<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(())
}
}

View file

@ -1,82 +0,0 @@
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)
}
}
}

View file

@ -1,159 +0,0 @@
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());
}
}

File diff suppressed because it is too large Load diff

1
src/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/bin

87
src/Makefile Normal file
View file

@ -0,0 +1,87 @@
PROJECT = magic_mount
VERSION ?= 0.1.0
# use zig cc
ZIG ?= zig
CC := $(ZIG) cc
STRIPPER := strip
# source files
SRCS := utils.c magic_mount.c main.c
# output directory
OUTDIR := bin
# common flags
CFLAGS_COMMON := -D_POSIX_C_SOURCE=200809L -std=c23 -Wall -Wextra -pedantic
CFLAGS_COMMON += -DVERSION=\"$(VERSION)\"
LDFLAGS_COMMON := -static
# build mode specific flags
CFLAGS_RELEASE := -O2 -DNDEBUG
CFLAGS_DEBUG := -O0 -g -DDEBUG
# zig target triples
TARGET_AMD64 ?= x86_64-linux-musl
TARGET_ARM64 ?= aarch64-linux-musl
TARGET_ARMV7 ?= arm-linux-musleabihf
# output binary names
BIN_AMD64 := $(OUTDIR)/mm_amd64
BIN_ARM64 := $(OUTDIR)/mm_arm64
BIN_ARMV7 := $(OUTDIR)/mm_armv7
BINS := $(BIN_AMD64) $(BIN_ARM64) $(BIN_ARMV7)
.PHONY: all clean release debug amd64 arm64 armv7 dirs help strip-bins
# default target
all: release
help:
@echo "Usage:"
@echo " make release [version=X.Y.Z] - Build release version (optimized, stripped)"
@echo " make debug [version=X.Y.Z] - Build debug version (with symbols)"
@echo " make clean - Clean build artifacts"
@echo ""
@echo "Individual targets:"
@echo " make amd64 - Build for x86_64"
@echo " make arm64 - Build for aarch64"
@echo " make armv7 - Build for armv7"
dirs:
mkdir -p $(OUTDIR)
# Release build (default)
release: export CFLAGS := $(CFLAGS_COMMON) $(CFLAGS_RELEASE)
release: dirs $(BINS) strip-bins
@echo "Release build completed: $(VERSION)"
# Debug build
debug: export CFLAGS := $(CFLAGS_COMMON) $(CFLAGS_DEBUG)
debug: dirs $(BINS)
@echo "Debug build completed: $(VERSION)"
strip-bins:
@echo "Stripping binaries..."
@for bin in $(BINS); do \
echo " Stripping $$bin"; \
$(STRIPPER) $$bin; \
done
amd64: $(BIN_AMD64)
arm64: $(BIN_ARM64)
armv7: $(BIN_ARMV7)
$(BIN_AMD64): $(SRCS)
$(CC) -target $(TARGET_AMD64) $(CFLAGS) $^ -o $@ $(LDFLAGS_COMMON)
$(BIN_ARM64): $(SRCS)
$(CC) -target $(TARGET_ARM64) $(CFLAGS) $^ -o $@ $(LDFLAGS_COMMON)
$(BIN_ARMV7): $(SRCS)
$(CC) -target $(TARGET_ARMV7) $(CFLAGS) $^ -o $@ $(LDFLAGS_COMMON)
clean:
rm -rf $(OUTDIR)

702
src/magic_mount.c Normal file
View file

@ -0,0 +1,702 @@
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mount.h>
#include <sys/xattr.h>
#include <dirent.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "magic_mount.h"
#include "utils.h"
typedef enum {
NFT_REGULAR,
NFT_DIRECTORY,
NFT_SYMLINK,
NFT_WHITEOUT
} NodeType;
typedef struct Node {
char *name;
NodeType type;
struct Node **children;
size_t child_count;
char *module_path;
char *module_name;
bool replace;
bool skip;
bool done;
} Node;
typedef struct {
int modules_total;
int nodes_total;
int nodes_mounted;
int nodes_skipped;
int nodes_whiteout;
int nodes_fail;
} Stats;
static Stats g_stats = {0};
Config g_config = {
.module_dir = DEFAULT_MODULE_DIR,
.mount_source = MOUNT_SOURCE,
.log_file = NULL,
.log_level = 2,
.failed_modules = NULL,
.failed_count = 0,
.extra_parts = NULL,
.extra_count = 0
};
void magic_log_write(LogLevel level, const char *fmt, ...) {
if ((int)level > g_config.log_level) return;
static const char *level_str[] = {"ERROR", "WARN", "INFO", "DEBUG"};
FILE *out = g_config.log_file ? g_config.log_file : stderr;
fprintf(out, "[%s] ", level_str[level]);
va_list ap;
va_start(ap, fmt);
vfprintf(out, fmt, ap);
va_end(ap);
fputc('\n', out);
fflush(out);
}
static int set_selinux_context(const char *path, const char *ctx) {
if (!path || !ctx) return 0;
LOGD("set_selinux(%s, \"%s\")", path, ctx);
if (lsetxattr(path, XATTR_SELINUX, ctx, strlen(ctx), 0) < 0) {
LOGW("set_selinux %s: %s", path, strerror(errno));
return -1;
}
return 0;
}
static char *get_selinux_context(const char *path) {
if (!path) return NULL;
ssize_t sz = lgetxattr(path, XATTR_SELINUX, NULL, 0);
if (sz <= 0) return NULL;
char *buf = malloc((size_t)sz + 1);
if (!buf) return NULL;
if (lgetxattr(path, XATTR_SELINUX, buf, (size_t)sz) != sz) {
free(buf);
return NULL;
}
buf[sz] = '\0';
LOGD("get_selinux(%s) -> \"%s\"", path, buf);
return buf;
}
static void copy_selinux_context(const char *src, const char *dst) {
char *ctx = get_selinux_context(src);
if (ctx) {
set_selinux_context(dst, ctx);
free(ctx);
}
}
static Node *node_new(const char *name, NodeType type) {
Node *n = calloc(1, sizeof(Node));
if (!n) return NULL;
n->name = strdup(name ? name : "");
n->type = type;
return n;
}
static void node_free(Node *n) {
if (!n) return;
for (size_t i = 0; i < n->child_count; i++)
node_free(n->children[i]);
free(n->children);
free(n->name);
free(n->module_path);
free(n->module_name);
free(n);
}
static NodeType get_node_type(const struct stat *st) {
if (S_ISCHR(st->st_mode) && st->st_rdev == 0) return NFT_WHITEOUT;
if (S_ISREG(st->st_mode)) return NFT_REGULAR;
if (S_ISDIR(st->st_mode)) return NFT_DIRECTORY;
if (S_ISLNK(st->st_mode)) return NFT_SYMLINK;
return NFT_WHITEOUT;
}
static bool is_dir_opaque(const char *path) {
#ifdef __linux__
char buf[8];
ssize_t len = lgetxattr(path, XATTR_OPAQUE, buf, sizeof(buf) - 1);
return len > 0 && buf[0] == 'y';
#else
(void)path;
return false;
#endif
}
static int node_add_child(Node *parent, Node *child) {
Node **arr = realloc(parent->children, (parent->child_count + 1) * sizeof(Node*));
if (!arr) return -1;
parent->children = arr;
parent->children[parent->child_count++] = child;
return 0;
}
static Node *node_find_child(Node *parent, const char *name) {
for (size_t i = 0; i < parent->child_count; i++)
if (strcmp(parent->children[i]->name, name) == 0)
return parent->children[i];
return NULL;
}
static Node *node_take_child(Node *parent, const char *name) {
for (size_t i = 0; i < parent->child_count; i++) {
if (strcmp(parent->children[i]->name, name) == 0) {
Node *n = parent->children[i];
memmove(&parent->children[i], &parent->children[i + 1],
(parent->child_count - i - 1) * sizeof(Node*));
parent->child_count--;
return n;
}
}
return NULL;
}
static Node *create_node_from_path(const char *name, const char *path,
const char *module_name) {
struct stat st;
if (lstat(path, &st) < 0) return NULL;
if (!(S_ISCHR(st.st_mode) || S_ISREG(st.st_mode) ||
S_ISDIR(st.st_mode) || S_ISLNK(st.st_mode)))
return NULL;
NodeType type = get_node_type(&st);
Node *n = node_new(name, type);
if (!n) return NULL;
n->module_path = strdup(path);
n->module_name = module_name ? strdup(module_name) : NULL;
n->replace = (type == NFT_DIRECTORY) && is_dir_opaque(path);
g_stats.nodes_total++;
return n;
}
static bool is_module_disabled(const char *mod_dir) {
char buf[PATH_MAX];
const char *disable_files[] = {DISABLE_FILE, REMOVE_FILE, SKIP_MOUNT_FILE};
for (size_t i = 0; i < ARR_LEN(disable_files); i++) {
if (path_join(buf, sizeof(buf), mod_dir, disable_files[i]) == 0 &&
path_exists(buf))
return true;
}
return false;
}
static int collect_module_files(Node *node, const char *dir,
const char *module_name, bool *has_any) {
DIR *d = opendir(dir);
if (!d) {
LOGE("opendir %s: %s", dir, strerror(errno));
return -1;
}
char path[PATH_MAX];
struct dirent *de;
bool any = false;
while ((de = readdir(d))) {
if (is_dot_or_dotdot(de->d_name)) continue;
if (path_join(path, sizeof(path), dir, de->d_name) != 0) {
closedir(d);
return -1;
}
Node *child = node_find_child(node, de->d_name);
if (!child) {
child = create_node_from_path(de->d_name, path, module_name);
if (child && node_add_child(node, child) != 0) {
node_free(child);
closedir(d);
return -1;
}
}
if (child && child->type == NFT_DIRECTORY) {
bool sub = false;
if (collect_module_files(child, path, module_name, &sub) != 0) {
closedir(d);
return -1;
}
if (sub || child->replace) any = true;
} else if (child) {
any = true;
}
}
closedir(d);
*has_any = any;
return 0;
}
static int add_partition_nodes(Node *root, Node *system) {
struct { const char *name; bool need_symlink; } parts[] = {
{"vendor", true},
{"system_ext", true},
{"product", true},
{"odm", false}
};
char root_path[PATH_MAX], sys_path[PATH_MAX];
for (size_t i = 0; i < ARR_LEN(parts); i++) {
if (path_join(root_path, sizeof(root_path), "/", parts[i].name) != 0 ||
path_join(sys_path, sizeof(sys_path), "/system", parts[i].name) != 0)
return -1;
if (!is_dir(root_path)) continue;
if (parts[i].need_symlink && !is_symlink(sys_path)) continue;
Node *child = node_take_child(system, parts[i].name);
if (child && node_add_child(root, child) != 0) {
node_free(child);
return -1;
}
}
for (int i = 0; i < g_config.extra_count; i++) {
if (path_join(root_path, sizeof(root_path), "/", g_config.extra_parts[i]) != 0)
return -1;
if (!is_dir(root_path)) continue;
Node *child = node_take_child(system, g_config.extra_parts[i]);
if (child && node_add_child(root, child) != 0) {
node_free(child);
return -1;
}
}
return 0;
}
static Node *scan_modules(void) {
Node *root = node_new("", NFT_DIRECTORY);
Node *system = node_new("system", NFT_DIRECTORY);
if (!root || !system) {
node_free(root);
node_free(system);
return NULL;
}
DIR *d = opendir(g_config.module_dir);
if (!d) {
LOGE("opendir %s: %s", g_config.module_dir, strerror(errno));
node_free(root);
node_free(system);
return NULL;
}
char mod_path[PATH_MAX], sys_path[PATH_MAX];
struct dirent *de;
bool has_any = false;
while ((de = readdir(d))) {
if (is_dot_or_dotdot(de->d_name)) continue;
if (path_join(mod_path, sizeof(mod_path), g_config.module_dir, de->d_name) != 0)
continue;
if (!is_dir(mod_path) || is_module_disabled(mod_path)) continue;
if (path_join(sys_path, sizeof(sys_path), mod_path, "system") != 0)
continue;
if (!is_dir(sys_path)) continue;
LOGD("scanning module: %s", de->d_name);
g_stats.modules_total++;
bool sub = false;
if (collect_module_files(system, sys_path, de->d_name, &sub) == 0 && sub)
has_any = true;
}
closedir(d);
if (!has_any) {
node_free(root);
node_free(system);
return NULL;
}
g_stats.nodes_total += 2; /* root + system */
if (add_partition_nodes(root, system) != 0) {
node_free(root);
node_free(system);
return NULL;
}
if (node_add_child(root, system) != 0) {
node_free(root);
node_free(system);
return NULL;
}
return root;
}
static int clone_symlink(const char *src, const char *dst) {
char target[PATH_MAX];
ssize_t len = readlink(src, target, sizeof(target) - 1);
if (len < 0) {
LOGE("readlink %s: %s", src, strerror(errno));
return -1;
}
target[len] = '\0';
if (symlink(target, dst) < 0) {
LOGE("symlink %s->%s: %s", dst, target, strerror(errno));
return -1;
}
copy_selinux_context(src, dst);
LOGD("clone_symlink: %s -> %s", dst, target);
return 0;
}
static int mirror_file(const char *src, const char *dst, const struct stat *st) {
int fd = open(dst, O_CREAT | O_WRONLY, st->st_mode & 07777);
if (fd < 0) {
LOGE("create %s: %s", dst, strerror(errno));
return -1;
}
close(fd);
if (mount(src, dst, NULL, MS_BIND, NULL) < 0) {
LOGE("bind %s->%s: %s", src, dst, strerror(errno));
return -1;
}
return 0;
}
static int mirror_dir(const char *src, const char *dst, const struct stat *st);
static int mirror_entry(const char *src_base, const char *dst_base, const char *name) {
char src[PATH_MAX], dst[PATH_MAX];
if (path_join(src, sizeof(src), src_base, name) != 0 ||
path_join(dst, sizeof(dst), dst_base, name) != 0)
return -1;
struct stat st;
if (lstat(src, &st) < 0) {
LOGW("lstat %s: %s", src, strerror(errno));
return 0;
}
if (S_ISREG(st.st_mode)) return mirror_file(src, dst, &st);
else if (S_ISDIR(st.st_mode)) return mirror_dir(src, dst, &st);
else if (S_ISLNK(st.st_mode)) return clone_symlink(src, dst);
return 0;
}
static int mirror_dir(const char *src, const char *dst, const struct stat *st) {
if (mkdir(dst, st->st_mode & 07777) < 0 && errno != EEXIST) {
LOGE("mkdir %s: %s", dst, strerror(errno));
return -1;
}
chmod(dst, st->st_mode & 07777);
chown(dst, st->st_uid, st->st_gid);
copy_selinux_context(src, dst);
DIR *d = opendir(src);
if (!d) {
LOGE("opendir %s: %s", src, strerror(errno));
return -1;
}
struct dirent *de;
int ret = 0;
while ((de = readdir(d))) {
if (is_dot_or_dotdot(de->d_name)) continue;
if (mirror_entry(src, dst, de->d_name) != 0) {
ret = -1;
break;
}
}
closedir(d);
return ret;
}
static int mount_regular_file(Node *node, const char *path,
const char *wpath, bool has_tmpfs) {
const char *target = has_tmpfs ? wpath : path;
if (has_tmpfs) {
int fd = open(wpath, O_CREAT | O_WRONLY, 0644);
if (fd < 0) {
LOGE("create %s: %s", wpath, strerror(errno));
return -1;
}
close(fd);
}
if (!node->module_path) {
LOGE("no module_path for %s", path);
return -1;
}
LOGD("bind %s -> %s", node->module_path, target);
if (mount(node->module_path, target, NULL, MS_BIND, NULL) < 0) {
LOGE("bind %s->%s: %s", node->module_path, target, strerror(errno));
return -1;
}
mount(NULL, target, NULL, MS_REMOUNT | MS_BIND | MS_RDONLY, NULL);
g_stats.nodes_mounted++;
return 0;
}
static int mount_directory(Node *node, const char *path,
const char *wpath, bool has_tmpfs);
static int process_child(Node *child, const char *path,
const char *wpath, bool has_tmpfs) {
if (child->skip) {
child->done = true;
return 0;
}
child->done = true;
return mount_directory(child, path, wpath, has_tmpfs);
}
static bool need_tmpfs_for_child(Node *child, const char *real_path) {
char path[PATH_MAX];
if (path_join(path, sizeof(path), real_path, child->name) != 0)
return false;
if (child->type == NFT_SYMLINK) return true;
if (child->type == NFT_WHITEOUT) return path_exists(path);
struct stat st;
if (lstat(path, &st) < 0) return true;
NodeType real_type = get_node_type(&st);
return real_type != child->type || real_type == NFT_SYMLINK;
}
static bool check_need_tmpfs(Node *node, const char *path, bool has_tmpfs) {
if (!has_tmpfs && node->replace && node->module_path)
return true;
for (size_t i = 0; i < node->child_count; i++)
if (need_tmpfs_for_child(node->children[i], path))
return true;
return false;
}
static int setup_tmpfs_dir(const char *wpath, const char *path,
const char *module_path) {
if (mkdir_recursive(wpath) != 0) return -1;
struct stat st;
const char *meta = NULL;
if (stat(path, &st) == 0) meta = path;
else if (module_path && stat(module_path, &st) == 0) meta = module_path;
else {
LOGE("no metadata for %s", path);
return -1;
}
chmod(wpath, st.st_mode & 07777);
chown(wpath, st.st_uid, st.st_gid);
copy_selinux_context(meta, wpath);
return 0;
}
static int mount_directory(Node *node, const char *base_path,
const char *base_work, bool has_tmpfs) {
char path[PATH_MAX], wpath[PATH_MAX];
if (path_join(path, sizeof(path), base_path, node->name) != 0 ||
path_join(wpath, sizeof(wpath), base_work, node->name) != 0)
return -1;
bool create_tmpfs = check_need_tmpfs(node, path, has_tmpfs);
bool now_tmpfs = has_tmpfs || create_tmpfs;
if (now_tmpfs && setup_tmpfs_dir(wpath, path, node->module_path) != 0)
return -1;
if (create_tmpfs &&
mount(wpath, wpath, NULL, MS_BIND, NULL) < 0) {
LOGE("bind self %s: %s", wpath, strerror(errno));
return -1;
}
/* 处理原目录内容 */
if (path_exists(path) && !node->replace) {
DIR *d = opendir(path);
if (d) {
struct dirent *de;
while ((de = readdir(d))) {
if (is_dot_or_dotdot(de->d_name)) continue;
Node *child = node_find_child(node, de->d_name);
int ret = child
? process_child(child, path, wpath, now_tmpfs)
: (now_tmpfs ? mirror_entry(path, wpath, de->d_name) : 0);
if (ret != 0) {
const char *mod = (child && child->module_name)
? child->module_name : node->module_name;
if (mod) {
LOGE("child %s/%s failed (module: %s)", path, de->d_name, mod);
add_to_array(&g_config.failed_modules,
&g_config.failed_count, mod);
}
g_stats.nodes_fail++;
if (now_tmpfs) {
closedir(d);
return -1;
}
}
}
closedir(d);
} else if (now_tmpfs) {
LOGE("opendir %s: %s", path, strerror(errno));
return -1;
}
}
for (size_t i = 0; i < node->child_count; i++) {
Node *child = node->children[i];
if (child->skip || child->done) continue;
if (process_child(child, path, wpath, now_tmpfs) != 0) {
const char *mod = child->module_name ? child->module_name : node->module_name;
if (mod) {
LOGE("child %s/%s failed (module: %s)", path, child->name, mod);
add_to_array(&g_config.failed_modules, &g_config.failed_count, mod);
}
g_stats.nodes_fail++;
if (now_tmpfs) return -1;
}
}
if (create_tmpfs) {
mount(NULL, wpath, NULL, MS_REMOUNT | MS_BIND | MS_RDONLY, NULL);
if (mount(wpath, path, NULL, MS_MOVE, NULL) < 0) {
LOGE("move %s->%s: %s", wpath, path, strerror(errno));
if (node->module_name)
add_to_array(&g_config.failed_modules,
&g_config.failed_count, node->module_name);
return -1;
}
LOGI("moved tmpfs: %s -> %s", wpath, path);
mount(NULL, path, NULL, MS_REC | MS_PRIVATE, NULL);
}
g_stats.nodes_mounted++;
return 0;
}
static int mount_node(Node *node, const char *path,
const char *wpath, bool has_tmpfs) {
switch (node->type) {
case NFT_REGULAR:
return mount_regular_file(node, path, wpath, has_tmpfs);
case NFT_SYMLINK:
if (!node->module_path) {
LOGE("no module_path for symlink %s", path);
return -1;
}
if (clone_symlink(node->module_path, wpath) == 0) {
g_stats.nodes_mounted++;
return 0;
}
return -1;
case NFT_WHITEOUT:
LOGD("whiteout: %s", path);
g_stats.nodes_whiteout++;
return 0;
case NFT_DIRECTORY:
return mount_directory(node, "/", wpath, has_tmpfs);
}
return 0;
}
const char *magic_mount_select_temp_dir(char *buf) {
const char *candidates[] = {"/mnt/vendor", "/mnt", "/debug_ramdisk"};
for (size_t i = 0; i < ARR_LEN(candidates); i++) {
if (!is_rw_tmpfs(candidates[i])) continue;
if (path_join(buf, PATH_MAX, candidates[i], ".magic_mount") == 0) {
LOGI("auto temp_dir: %s (from %s)", buf, candidates[i]);
return buf;
}
}
LOGW("no rw tmpfs, using fallback: %s", DEFAULT_TEMP_DIR);
return DEFAULT_TEMP_DIR;
}
int magic_mount_run(const char *temp_root) {
Node *root = scan_modules();
if (!root) {
LOGI("no modules found");
return 0;
}
char tmpfs_dir[PATH_MAX];
if (path_join(tmpfs_dir, sizeof(tmpfs_dir), temp_root, "workdir") != 0) {
node_free(root);
return -1;
}
if (mkdir_recursive(tmpfs_dir) != 0) {
node_free(root);
return -1;
}
LOGI("mounting tmpfs: %s (source=%s)", tmpfs_dir, g_config.mount_source);
if (mount(g_config.mount_source, tmpfs_dir, "tmpfs", 0, "") < 0) {
LOGE("mount tmpfs %s: %s", tmpfs_dir, strerror(errno));
node_free(root);
return -1;
}
mount(NULL, tmpfs_dir, NULL, MS_REC | MS_PRIVATE, NULL);
int ret = mount_node(root, "/", tmpfs_dir, false);
if (ret != 0) g_stats.nodes_fail++;
umount2(tmpfs_dir, MNT_DETACH);
rmdir(tmpfs_dir);
node_free(root);
return ret;
}
int magic_mount_enter_pid1_mntns(void) {
int fd = open("/proc/1/ns/mnt", O_RDONLY);
if (fd < 0) {
LOGE("open /proc/1/ns/mnt: %s", strerror(errno));
return -1;
}
if (setns(fd, 0) < 0) {
LOGE("setns: %s", strerror(errno));
close(fd);
return -1;
}
close(fd);
LOGI("entered pid1 mount namespace");
return 0;
}
void magic_mount_print_summary(void) {
LOGI("summary: modules=%d nodes=%d mounted=%d skipped=%d whiteouts=%d failures=%d",
g_stats.modules_total, g_stats.nodes_total, g_stats.nodes_mounted,
g_stats.nodes_skipped, g_stats.nodes_whiteout, g_stats.nodes_fail);
if (g_config.failed_count > 0) {
LOGW("failed modules (%d):", g_config.failed_count);
for (int i = 0; i < g_config.failed_count; i++)
LOGW(" - %s", g_config.failed_modules[i]);
}
}
void magic_mount_parse_partitions(const char *list) {
const char *p = list;
while (*p) {
const char *start = p;
while (*p && *p != ',') p++;
size_t len = (size_t)(p - start);
while (len > 0 && (start[0] == ' ' || start[0] == '\t')) {
start++;
len--;
}
while (len > 0 && (start[len-1] == ' ' || start[len-1] == '\t'))
len--;
if (len > 0) {
char *name = malloc(len + 1);
if (name) {
memcpy(name, start, len);
name[len] = '\0';
add_to_array(&g_config.extra_parts, &g_config.extra_count, name);
free(name);
}
}
if (*p == ',') p++;
}
}

55
src/magic_mount.h Normal file
View file

@ -0,0 +1,55 @@
#ifndef MAGIC_MOUNT_H
#define MAGIC_MOUNT_H
#include <stdio.h>
#define DEFAULT_MODULE_DIR "/data/adb/modules"
#define DEFAULT_TEMP_DIR "/dev/.magic_mount"
#define MOUNT_SOURCE "KSU"
#define DISABLE_FILE "disable"
#define REMOVE_FILE "remove"
#define SKIP_MOUNT_FILE "skip_mount"
#define XATTR_OPAQUE "trusted.overlay.opaque"
#define XATTR_SELINUX "security.selinux"
#define ARR_LEN(a) (sizeof(a) / sizeof((a)[0]))
typedef enum {
LOG_ERROR = 0,
LOG_WARN = 1,
LOG_INFO = 2,
LOG_DEBUG = 3
} LogLevel;
typedef struct {
const char *module_dir;
const char *mount_source;
FILE *log_file;
int log_level;
char **failed_modules;
int failed_count;
char **extra_parts;
int extra_count;
} Config;
extern Config g_config;
// shim
int setns(int fd, int nstype);
void magic_log_write(LogLevel level, const char *fmt, ...);
#define LOGE(...) magic_log_write(LOG_ERROR, __VA_ARGS__)
#define LOGW(...) magic_log_write(LOG_WARN, __VA_ARGS__)
#define LOGI(...) magic_log_write(LOG_INFO, __VA_ARGS__)
#define LOGD(...) magic_log_write(LOG_DEBUG, __VA_ARGS__)
const char *magic_mount_select_temp_dir(char *buf);
int magic_mount_run(const char *temp_root);
int magic_mount_enter_pid1_mntns(void);
void magic_mount_print_summary(void);
void magic_mount_parse_partitions(const char *list);
#endif /* MAGIC_MOUNT_H */

104
src/main.c Normal file
View file

@ -0,0 +1,104 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include "magic_mount.h"
#include "utils.h"
static void usage(const char *prog) {
fprintf(stderr,
"Magic Mount v%s\n"
"Usage: %s [options]\n"
" -m DIR module directory (default: %s)\n"
" -t DIR temp directory (default: auto)\n"
" -s SRC mount source (default: %s)\n"
" -p LIST extra partitions (-p mi_ext,my_stock)\n"
" -l FILE log file ('-' for stdout, default: stderr)\n"
" -v N log level (0-3, default: 2)\n"
" -h show this help\n",
VERSION, prog, DEFAULT_MODULE_DIR, MOUNT_SOURCE);
}
int main(int argc, char **argv) {
const char *temp_dir = NULL;
const char *log_path = NULL;
char auto_temp[PATH_MAX] = {0};
int ret = 1;
/* 解析参数 */
for (int i = 1; i < argc; i++) {
if ((strcmp(argv[i], "-m") == 0 || strcmp(argv[i], "--module-dir") == 0) &&
i + 1 < argc) {
g_config.module_dir = argv[++i];
} else if ((strcmp(argv[i], "-t") == 0 || strcmp(argv[i], "--temp-dir") == 0) &&
i + 1 < argc) {
temp_dir = argv[++i];
} else if ((strcmp(argv[i], "-s") == 0 || strcmp(argv[i], "--mount-source") == 0) &&
i + 1 < argc) {
g_config.mount_source = argv[++i];
} else if ((strcmp(argv[i], "-l") == 0 || strcmp(argv[i], "--log-file") == 0) &&
i + 1 < argc) {
log_path = argv[++i];
} else if ((strcmp(argv[i], "-v") == 0 || strcmp(argv[i], "--verbose") == 0) &&
i + 1 < argc) {
g_config.log_level = atoi(argv[++i]);
if (g_config.log_level < 0) g_config.log_level = 0;
if (g_config.log_level > 3) g_config.log_level = 3;
} else if ((strcmp(argv[i], "-p") == 0 || strcmp(argv[i], "--partitions") == 0) &&
i + 1 < argc) {
magic_mount_parse_partitions(argv[++i]);
} else if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) {
usage(argv[0]);
return 0;
} else {
fprintf(stderr, "Unknown option: %s\n", argv[i]);
usage(argv[0]);
return 1;
}
}
/* 打开日志文件 */
if (log_path) {
g_config.log_file = strcmp(log_path, "-") == 0 ?
stdout : fopen(log_path, "a");
if (!g_config.log_file && strcmp(log_path, "-") != 0) {
fprintf(stderr, "Failed to open log: %s\n", strerror(errno));
return 1;
}
}
/* 选择临时目录 */
if (!temp_dir)
temp_dir = magic_mount_select_temp_dir(auto_temp);
/* 权限检查 */
if (geteuid() != 0) {
fprintf(stderr, "must run as root\n");
goto cleanup;
}
/* 进入 PID1 命名空间 */
if (magic_mount_enter_pid1_mntns() != 0)
goto cleanup;
/* 执行挂载 */
fprintf(stderr, "starting: module_dir=%s temp_dir=%s source=%s level=%d\n",
g_config.module_dir, temp_dir, g_config.mount_source, g_config.log_level);
ret = magic_mount_run(temp_dir);
fprintf(stderr, "%s\n", ret == 0 ? "completed successfully" : "failed");
magic_mount_print_summary();
cleanup:
if (g_config.log_file && g_config.log_file != stdout &&
g_config.log_file != stderr)
fclose(g_config.log_file);
free_str_array(&g_config.failed_modules, &g_config.failed_count);
free_str_array(&g_config.extra_parts, &g_config.extra_count);
return ret == 0 ? 0 : 1;
}

87
src/utils.c Normal file
View file

@ -0,0 +1,87 @@
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/statfs.h>
#include <dirent.h>
#include <unistd.h>
#include <errno.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include "utils.h"
int path_join(char *buf, size_t n, const char *base, const char *name) {
if (!base) return -1;
if (!name || !name[0]) return snprintf(buf, n, "%s", base) >= (int)n ? -1 : 0;
size_t len = strlen(base);
bool has_slash = (len > 0 && base[len-1] == '/');
return snprintf(buf, n, has_slash ? "%s%s" : "%s/%s", base, name) >= (int)n ? -1 : 0;
}
bool path_exists(const char *p) { return access(p, F_OK) == 0; }
bool is_dir(const char *p) {
struct stat st;
return stat(p, &st) == 0 && S_ISDIR(st.st_mode);
}
bool is_symlink(const char *p) {
struct stat st;
return lstat(p, &st) == 0 && S_ISLNK(st.st_mode);
}
int mkdir_recursive(const char *dir) {
if (!dir || !dir[0]) return -1;
if (is_dir(dir)) return 0;
char tmp[PATH_MAX];
if (snprintf(tmp, sizeof(tmp), "%s", dir) >= (int)sizeof(tmp)) return -1;
char *p = strrchr(tmp, '/');
if (p && p != tmp) {
*p = 0;
if (mkdir_recursive(tmp) != 0) return -1;
}
return (mkdir(dir, 0755) == 0 || errno == EEXIST) ? 0 : -1;
}
bool is_rw_tmpfs(const char *path) {
struct statfs s;
if (!is_dir(path) || statfs(path, &s) < 0) return false;
if ((unsigned long)s.f_type != TMPFS_MAGIC) return false;
char tmp[PATH_MAX];
if (path_join(tmp, sizeof(tmp), path, ".test_XXXXXX") != 0) return false;
int fd = mkstemp(tmp);
if (fd < 0) return false;
close(fd);
unlink(tmp);
return true;
}
void free_str_array(char ***arr, int *count) {
if (!arr || !*arr) return;
for (int i = 0; i < *count; i++) free((*arr)[i]);
free(*arr);
*arr = NULL;
*count = 0;
}
void add_to_array(char ***arr, int *count, const char *str) {
for (int i = 0; i < *count; i++)
if (strcmp((*arr)[i], str) == 0) return;
char **new_arr = realloc(*arr, (size_t)(*count + 1) * sizeof(char*));
if (!new_arr) return;
*arr = new_arr;
(*arr)[*count] = strdup(str);
if ((*arr)[*count]) (*count)++;
}
bool is_dot_or_dotdot(const char *n) {
return n && (!strcmp(n, ".") || !strcmp(n, ".."));
}

27
src/utils.h Normal file
View file

@ -0,0 +1,27 @@
#ifndef UTILS_H
#define UTILS_H
#include <stdbool.h>
#include <stddef.h>
#ifndef PATH_MAX
#define PATH_MAX 4096
#endif
#ifndef TMPFS_MAGIC
#define TMPFS_MAGIC 0x01021994
#endif
int path_join(char *buf, size_t n, const char *base, const char *name);
bool path_exists(const char *p);
bool is_dir(const char *p);
bool is_symlink(const char *p);
int mkdir_recursive(const char *dir);
bool is_rw_tmpfs(const char *path);
void free_str_array(char ***arr, int *count);
void add_to_array(char ***arr, int *count, const char *str);
bool is_dot_or_dotdot(const char *n);
#endif /* UTILS_H */

47
template/customize.sh Normal file
View file

@ -0,0 +1,47 @@
#!/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_armv7"
;;
x86_64)
ui_print "- Selected architecture: AMD64"
ARCH_BINARY="mm_amd64"
;;
*)
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/bin/$ARCH_BINARY" "$MODPATH/meta-mm" || abort "! Failed to rename binary"
# Remove the unused binary
rm -rf "$MODPATH/bin"
# 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"

33
template/metainstall.sh Normal file
View file

@ -0,0 +1,33 @@
#!/system/bin/sh
############################################
# meta-mm metainstall.sh
############################################
export KSU_HAS_METAMODULE="true"
export KSU_METAMODULE="meta-mm"
# Main installation flow
ui_print "- Using meta-mm metainstall"
# undo_handle_partition
# because ksu moves them e.g. MODDIR/system/product to MODDIR/product
# this way we can support normal hierarchy that ksu breaks
undo_handle_partition() {
partition_to_undo="$1"
if [ -L "$MODPATH/system/$partition_to_undo" ] && [ -d "$MODPATH/$partition_to_undo" ]; then
# ui_print "- undo handle_partition for /$partition_to_undo"
rm -f "$MODPATH/system/$partition_to_undo"
mv -f "$MODPATH/$partition_to_undo" "$MODPATH/system/$partition_to_undo"
fi
}
# call install function, this is important!
install_module
# Run for typical partitions
undo_handle_partition vendor
undo_handle_partition product
undo_handle_partition system_ext
undo_handle_partition odm
ui_print "- Installation complete"

36
template/metamount.sh Normal file
View file

@ -0,0 +1,36 @@
#!/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_METADATA_SOURCE="KSU"
MODULE_METADATA_LOGLEVEL=3
MODULE_METADATA_LOGFILE="/data/adb/mm.log"
if [ -f "$MODULE_METADATA_LOGFILE" ]; then
mv "$MODULE_METADATA_LOGFILE" "$MODULE_METADATA_LOGFILE".old
fi
# Execute the mount binary
"$BINARY" -s "$MODULE_METADATA_SOURCE" -m "$MODULE_METADATA_DIR" -v "$MODULE_METADATA_LOGLEVEL" -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

View file

@ -0,0 +1,6 @@
#!/system/bin/sh
############################################
# mm-mm metauninstall.sh
############################################
exit 0

8
template/module.prop Normal file
View file

@ -0,0 +1,8 @@
id=meta-mm
metamodule=1
name=Magic Mount MetaModule
version=v0.1.0
versionCode=2511201925
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

Some files were not shown because too many files have changed in this diff Show more