mirror of
https://github.com/topjohnwu/Magisk.git
synced 2026-07-03 14:08:39 +02:00
7b2d40987c
Magisk binary mounting logic is not very clear now. In this commit, it is rewritten in a more robust way. Now it has following cases in mind: 1) Device has a su binary, magisk need to overlay it 2) Choose the PATH with least files to reduce bind mount 3) Filter path which is not suitable
617 lines
21 KiB
Rust
617 lines
21 KiB
Rust
use crate::consts::{MODULEMNT, WORKERDIR};
|
|
use crate::ffi::{ModuleInfo, get_magisk_tmp};
|
|
use crate::load_prop_file;
|
|
use base::{
|
|
Directory, FsPathBuilder, LoggedResult, OsResultStatic, ResultExt, Utf8CStr, Utf8CStrBuf,
|
|
Utf8CString, WalkResult, clone_attr, cstr, debug, error, info, libc, warn,
|
|
};
|
|
use libc::{MS_RDONLY, O_CLOEXEC, O_CREAT, O_RDONLY};
|
|
use std::collections::BTreeMap;
|
|
use std::path::{Component, Path};
|
|
|
|
const MAGISK_BIN_INJECT_PARTITIONS: [&Utf8CStr; 4] =
|
|
[cstr!("/system/"), cstr!("/vendor/"), cstr!("/product/"), cstr!("/system_ext/")];
|
|
|
|
const SECONDARY_READ_ONLY_PARTITIONS: [&Utf8CStr; 3] =
|
|
[cstr!("/vendor"), cstr!("/product"), cstr!("/system_ext")];
|
|
|
|
type FsNodeMap = BTreeMap<String, FsNode>;
|
|
|
|
macro_rules! module_log {
|
|
($($args:tt)+) => {
|
|
debug!("{:8}: {} <- {}", $($args)+)
|
|
}
|
|
}
|
|
|
|
#[allow(unused_variables)]
|
|
fn bind_mount(reason: &str, src: &Utf8CStr, dest: &Utf8CStr, rec: bool) -> OsResultStatic<()> {
|
|
module_log!(reason, dest, src);
|
|
src.bind_mount_to(dest, rec)?;
|
|
dest.remount_mount_point_flags(MS_RDONLY)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn mount_dummy(reason: &str, src: &Utf8CStr, dest: &Utf8CStr, is_dir: bool) -> OsResultStatic<()> {
|
|
if is_dir {
|
|
dest.mkdir(0o000)?;
|
|
} else {
|
|
dest.create(O_CREAT | O_RDONLY | O_CLOEXEC, 0o000)?;
|
|
}
|
|
bind_mount(reason, src, dest, false)
|
|
}
|
|
|
|
// File paths that act like a stack, popping out the last element
|
|
// automatically when out of scope. Using Rust's lifetime mechanism,
|
|
// we can ensure the buffer will never be incorrectly copied or modified.
|
|
// After calling append or clone, the mutable reference's lifetime is
|
|
// "transferred" to the returned object, and the compiler will guarantee
|
|
// that the original mutable reference can only be reused if and only if
|
|
// the newly created instance is destroyed.
|
|
struct PathTracker<'a> {
|
|
real: &'a mut dyn Utf8CStrBuf,
|
|
tmp: &'a mut dyn Utf8CStrBuf,
|
|
real_len: usize,
|
|
tmp_len: usize,
|
|
}
|
|
|
|
impl PathTracker<'_> {
|
|
fn from<'a>(real: &'a mut dyn Utf8CStrBuf, tmp: &'a mut dyn Utf8CStrBuf) -> PathTracker<'a> {
|
|
let real_len = real.len();
|
|
let tmp_len = tmp.len();
|
|
PathTracker {
|
|
real,
|
|
tmp,
|
|
real_len,
|
|
tmp_len,
|
|
}
|
|
}
|
|
|
|
fn append(&mut self, name: &str) -> PathTracker {
|
|
let real_len = self.real.len();
|
|
let tmp_len = self.tmp.len();
|
|
self.real.append_path(name);
|
|
self.tmp.append_path(name);
|
|
PathTracker {
|
|
real: self.real,
|
|
tmp: self.tmp,
|
|
real_len,
|
|
tmp_len,
|
|
}
|
|
}
|
|
|
|
fn clone(&mut self) -> PathTracker {
|
|
Self::from(self.real, self.tmp)
|
|
}
|
|
}
|
|
|
|
impl Drop for PathTracker<'_> {
|
|
// Revert back to the original state after finish using the buffer
|
|
fn drop(&mut self) {
|
|
self.real.truncate(self.real_len);
|
|
self.tmp.truncate(self.tmp_len);
|
|
}
|
|
}
|
|
|
|
enum FsNode {
|
|
Directory { children: FsNodeMap },
|
|
File { src: Utf8CString },
|
|
Symlink { target: Utf8CString, is_magisk_bin: bool },
|
|
Whiteout,
|
|
}
|
|
|
|
impl FsNode {
|
|
fn new_dir() -> FsNode {
|
|
FsNode::Directory {
|
|
children: BTreeMap::new(),
|
|
}
|
|
}
|
|
|
|
fn build_from_path(&mut self, path: &mut dyn Utf8CStrBuf) -> LoggedResult<()> {
|
|
let FsNode::Directory { children } = self else {
|
|
return Ok(());
|
|
};
|
|
let mut dir = Directory::open(path)?;
|
|
let path_len = path.len();
|
|
|
|
while let Some(entry) = dir.read()? {
|
|
path.truncate(path_len);
|
|
path.append_path(entry.name());
|
|
if entry.is_dir() {
|
|
let node = children
|
|
.entry(entry.name().to_string())
|
|
.or_insert_with(FsNode::new_dir);
|
|
node.build_from_path(path)?;
|
|
} else if entry.is_symlink() {
|
|
let mut link = cstr::buf::default();
|
|
path.read_link(&mut link)?;
|
|
children
|
|
.entry(entry.name().to_string())
|
|
.or_insert_with(|| FsNode::Symlink {
|
|
target: link.to_owned(),
|
|
is_magisk_bin: false,
|
|
});
|
|
} else {
|
|
if entry.is_char_device() {
|
|
let attr = path.get_attr()?;
|
|
if attr.is_whiteout() {
|
|
children
|
|
.entry(entry.name().to_string())
|
|
.or_insert_with(|| FsNode::Whiteout);
|
|
continue;
|
|
}
|
|
}
|
|
children
|
|
.entry(entry.name().to_string())
|
|
.or_insert_with(|| FsNode::File {
|
|
src: path.to_owned(),
|
|
});
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
// The parent node has to be tmpfs if:
|
|
// - Target does not exist
|
|
// - Source or target is a symlink (since we cannot bind mount symlink)
|
|
// - Source is whiteout (used for removal)
|
|
fn parent_should_be_tmpfs(&self, target_path: &Utf8CStr) -> bool {
|
|
match self {
|
|
FsNode::Directory { .. } | FsNode::File { .. } => {
|
|
if let Ok(attr) = target_path.get_attr() {
|
|
attr.is_symlink()
|
|
} else {
|
|
true
|
|
}
|
|
}
|
|
FsNode::Symlink { .. } | FsNode::Whiteout => true,
|
|
}
|
|
}
|
|
|
|
fn children(&mut self) -> Option<&mut FsNodeMap> {
|
|
match self {
|
|
FsNode::Directory { children } => Some(children),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn commit(&mut self, mut path: PathTracker, is_root_dir: bool) -> LoggedResult<()> {
|
|
match self {
|
|
FsNode::Directory { children } => {
|
|
let mut is_tmpfs = false;
|
|
|
|
// First determine whether tmpfs is required
|
|
children.retain(|name, node| {
|
|
if name == ".replace" {
|
|
return if is_root_dir {
|
|
warn!("Unable to replace '{}', ignore request", path.real);
|
|
false
|
|
} else {
|
|
is_tmpfs = true;
|
|
true
|
|
};
|
|
}
|
|
|
|
let path = path.append(name);
|
|
if node.parent_should_be_tmpfs(path.real) {
|
|
if is_root_dir {
|
|
// Ignore the unsupported child node
|
|
warn!("Unable to add '{}', skipped", path.real);
|
|
return false;
|
|
}
|
|
is_tmpfs = true;
|
|
}
|
|
true
|
|
});
|
|
|
|
if is_tmpfs {
|
|
self.commit_tmpfs(path.clone())?;
|
|
// Transitioning from non-tmpfs to tmpfs, we need to actually mount the
|
|
// worker dir to dest after all children are committed.
|
|
bind_mount("move", path.tmp, path.real, true)?;
|
|
} else {
|
|
for (name, node) in children {
|
|
let path = path.append(name);
|
|
node.commit(path, false)?;
|
|
}
|
|
}
|
|
}
|
|
FsNode::File { src } => {
|
|
clone_attr(path.real, src)?;
|
|
bind_mount("mount", src, path.real, false)?;
|
|
}
|
|
FsNode::Symlink { .. } | FsNode::Whiteout => {
|
|
error!("Unable to handle '{}': parent should be tmpfs", path.real);
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn commit_tmpfs(&mut self, mut path: PathTracker) -> LoggedResult<()> {
|
|
match self {
|
|
FsNode::Directory { children } => {
|
|
path.tmp.mkdirs(0o000)?;
|
|
if path.real.exists() {
|
|
clone_attr(path.real, path.tmp)?;
|
|
} else if let Some(p) = path.tmp.parent_dir() {
|
|
let parent = Utf8CString::from(p);
|
|
clone_attr(&parent, path.tmp)?;
|
|
}
|
|
|
|
// Check whether a file name '.replace' exist
|
|
if let Some(FsNode::File { src }) = children.remove(".replace")
|
|
&& let Some(base_dir) = src.parent_dir()
|
|
{
|
|
for (name, node) in children {
|
|
let path = path.append(name);
|
|
match node {
|
|
FsNode::Directory { .. } | FsNode::File { .. } => {
|
|
let src = Utf8CString::from(base_dir).join_path(name);
|
|
bind_mount(
|
|
"mount",
|
|
&src,
|
|
path.real,
|
|
matches!(node, FsNode::Directory { .. }),
|
|
)?;
|
|
}
|
|
_ => node.commit_tmpfs(path)?,
|
|
}
|
|
}
|
|
|
|
// If performing replace, we skip mirroring
|
|
return Ok(());
|
|
}
|
|
|
|
// Traverse the real directory and mount mirrors
|
|
if let Ok(mut dir) = Directory::open(path.real) {
|
|
while let Ok(Some(entry)) = dir.read() {
|
|
if children.contains_key(entry.name().as_str()) {
|
|
// Should not be mirrored, next
|
|
continue;
|
|
}
|
|
|
|
let path = path.append(entry.name());
|
|
|
|
if entry.is_symlink() {
|
|
// Add the symlink into children and handle it later
|
|
let mut link = cstr::buf::default();
|
|
entry.read_link(&mut link).log_ok();
|
|
children.insert(
|
|
entry.name().to_string(),
|
|
FsNode::Symlink {
|
|
target: link.to_owned(),
|
|
is_magisk_bin: false,
|
|
},
|
|
);
|
|
} else {
|
|
mount_dummy("mirror", path.real, path.tmp, entry.is_dir())?;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Finally, commit children
|
|
for (name, node) in children {
|
|
let path = path.append(name);
|
|
node.commit_tmpfs(path)?;
|
|
}
|
|
}
|
|
FsNode::File { src } => {
|
|
if path.real.exists() {
|
|
clone_attr(path.real, src)?;
|
|
}
|
|
mount_dummy("mount", src, path.tmp, false)?;
|
|
}
|
|
FsNode::Symlink { target, is_magisk_bin } => {
|
|
module_log!("mklink", path.tmp, target);
|
|
path.tmp.create_symlink_to(target)?;
|
|
// Avoid cloneing existing su attributes to our su
|
|
if !*is_magisk_bin && path.real.exists() {
|
|
clone_attr(path.real, path.tmp)?;
|
|
}
|
|
}
|
|
FsNode::Whiteout => {
|
|
module_log!("delete", path.real, "null");
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
fn get_path_env() -> String {
|
|
std::env::var_os("PATH")
|
|
.and_then(|s| s.into_string().ok())
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
fn inject_magisk_bins(system: &mut FsNode) {
|
|
fn inject(children: &mut FsNodeMap) {
|
|
let mut path = cstr::buf::default().join_path(get_magisk_tmp());
|
|
|
|
// Inject binaries
|
|
|
|
let len = path.len();
|
|
path.append_path("magisk");
|
|
children.insert(
|
|
"magisk".to_string(),
|
|
FsNode::File {
|
|
src: path.to_owned(),
|
|
},
|
|
);
|
|
|
|
path.truncate(len);
|
|
path.append_path("magiskpolicy");
|
|
children.insert(
|
|
"magiskpolicy".to_string(),
|
|
FsNode::File {
|
|
src: path.to_owned(),
|
|
},
|
|
);
|
|
|
|
// Inject applet symlinks
|
|
|
|
children.insert(
|
|
"su".to_string(),
|
|
FsNode::Symlink {
|
|
target: Utf8CString::from("./magisk"),
|
|
is_magisk_bin: true,
|
|
},
|
|
);
|
|
children.insert(
|
|
"resetprop".to_string(),
|
|
FsNode::Symlink {
|
|
target: Utf8CString::from("./magisk"),
|
|
is_magisk_bin: true,
|
|
},
|
|
);
|
|
children.insert(
|
|
"supolicy".to_string(),
|
|
FsNode::Symlink {
|
|
target: Utf8CString::from("./magiskpolicy"),
|
|
is_magisk_bin: true,
|
|
},
|
|
);
|
|
}
|
|
|
|
// Strip /system prefix to insert correct node
|
|
fn strip_system_prefix(orig_item: &str) -> String {
|
|
match orig_item.strip_prefix("/system/") {
|
|
Some(rest) => format!("/{}", rest),
|
|
None => orig_item.to_string(),
|
|
}
|
|
}
|
|
|
|
let path_env = get_path_env();
|
|
let mut candidates = vec![];
|
|
|
|
for orig_item in path_env.split(':') {
|
|
// Filter not suitbale paths
|
|
if !MAGISK_BIN_INJECT_PARTITIONS.iter().any(|p| orig_item.starts_with(p.as_str())) {
|
|
continue;
|
|
}
|
|
// Flatten apex path is not suitable too
|
|
if orig_item.starts_with("/system/apex/") {
|
|
continue;
|
|
}
|
|
|
|
// Override existing su first
|
|
let su_path = Utf8CString::from(format!("{}/su", orig_item));
|
|
if su_path.exists() {
|
|
let item = strip_system_prefix(orig_item);
|
|
candidates.push((item, 0));
|
|
break;
|
|
}
|
|
|
|
let path = Utf8CString::from(orig_item);
|
|
if let Ok(attr) = path.get_attr() && (attr.st.st_mode & 0x0001) != 0 {
|
|
if let Ok(mut dir) = Directory::open(&path) {
|
|
let mut count = 0;
|
|
if let Err(_) = dir.pre_order_walk(|e| {
|
|
if e.is_file() {
|
|
count += 1;
|
|
}
|
|
Ok(WalkResult::Continue)
|
|
}) {
|
|
// Skip, we cannot ensure the result is correct
|
|
continue;
|
|
}
|
|
let item = strip_system_prefix(orig_item);
|
|
candidates.push((item, count));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Sort by amount of files
|
|
candidates.sort_by_key(|&(_, count)| count);
|
|
|
|
'path_loop: for candidate in candidates {
|
|
let components = Path::new(&candidate.0)
|
|
.components()
|
|
.filter(|c| matches!(c, Component::Normal(_)))
|
|
.filter_map(|c| c.as_os_str().to_str());
|
|
|
|
let mut curr = match system {
|
|
FsNode::Directory { children } => children,
|
|
_ => continue,
|
|
};
|
|
|
|
for dir in components {
|
|
let node = curr.entry(dir.to_owned()).or_insert_with(FsNode::new_dir);
|
|
match node {
|
|
FsNode::Directory { children } => curr = children,
|
|
_ => continue 'path_loop,
|
|
}
|
|
}
|
|
|
|
// Found a suitable path, done
|
|
inject(curr);
|
|
return;
|
|
}
|
|
|
|
// If still not found, directly inject into /system/bin
|
|
let node = system
|
|
.children()
|
|
.map(|c| c.entry("bin".to_string()).or_insert_with(FsNode::new_dir));
|
|
if let Some(FsNode::Directory { children }) = node {
|
|
inject(children)
|
|
}
|
|
}
|
|
|
|
fn inject_zygisk_bins(system: &mut FsNode, name: &str) {
|
|
#[cfg(target_pointer_width = "64")]
|
|
let has_32_bit = cstr!("/system/bin/linker").exists();
|
|
|
|
#[cfg(target_pointer_width = "32")]
|
|
let has_32_bit = true;
|
|
|
|
if has_32_bit {
|
|
let lib = system
|
|
.children()
|
|
.map(|c| c.entry("lib".to_string()).or_insert_with(FsNode::new_dir));
|
|
if let Some(FsNode::Directory { children }) = lib {
|
|
let mut bin_path = cstr::buf::default().join_path(get_magisk_tmp());
|
|
|
|
#[cfg(target_pointer_width = "64")]
|
|
bin_path.append_path("magisk32");
|
|
|
|
#[cfg(target_pointer_width = "32")]
|
|
bin_path.append_path("magisk");
|
|
|
|
// There are some devices that announce ABI as 64 bit only, but ship with linker64
|
|
// because they make use of a special 32 bit to 64 bit translator (such as tango).
|
|
// In this case, magisk32 does not exist, so inserting it will cause bind mount
|
|
// failure and affect module mount. Native bridge injection does not support these
|
|
// kind of translators anyway, so simply check if magisk32 exists here.
|
|
if bin_path.exists() {
|
|
children.insert(
|
|
name.to_string(),
|
|
FsNode::File {
|
|
src: bin_path.to_owned(),
|
|
},
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(target_pointer_width = "64")]
|
|
if cstr!("/system/bin/linker64").exists() {
|
|
let lib64 = system
|
|
.children()
|
|
.map(|c| c.entry("lib64".to_string()).or_insert_with(FsNode::new_dir));
|
|
if let Some(FsNode::Directory { children }) = lib64 {
|
|
let bin_path = cstr::buf::default()
|
|
.join_path(get_magisk_tmp())
|
|
.join_path("magisk");
|
|
|
|
children.insert(
|
|
name.to_string(),
|
|
FsNode::File {
|
|
src: bin_path.to_owned(),
|
|
},
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn load_modules(module_list: &[ModuleInfo], zygisk_name: &str) {
|
|
let mut system = FsNode::new_dir();
|
|
|
|
// Step 1: Create virtual filesystem tree
|
|
//
|
|
// In this step, there is zero logic applied during tree construction; we simply collect
|
|
// and record the union of all module filesystem trees under each of their /system directory.
|
|
|
|
let mut path = cstr::buf::default()
|
|
.join_path(get_magisk_tmp())
|
|
.join_path(MODULEMNT);
|
|
let len = path.len();
|
|
for info in module_list {
|
|
path.truncate(len);
|
|
path.append_path(&info.name);
|
|
|
|
// Read props
|
|
let module_path_len = path.len();
|
|
path.append_path("system.prop");
|
|
if path.exists() {
|
|
// Do NOT go through property service as it could cause boot lock
|
|
load_prop_file(&path, true);
|
|
}
|
|
|
|
// Check whether skip mounting
|
|
path.truncate(module_path_len);
|
|
path.append_path("skip_mount");
|
|
if path.exists() {
|
|
continue;
|
|
}
|
|
|
|
// Double check whether the system folder exists
|
|
path.truncate(module_path_len);
|
|
path.append_path("system");
|
|
if path.exists() {
|
|
info!("{}: loading module files", &info.name);
|
|
system.build_from_path(&mut path).log_ok();
|
|
}
|
|
}
|
|
|
|
// Step 2: Inject custom files
|
|
//
|
|
// Magisk provides some built-in functionality that requires augmenting the filesystem.
|
|
// We expose several cmdline tools (e.g. su) into PATH, and the zygisk shared library
|
|
// has to also be added into the default LD_LIBRARY_PATH for code injection.
|
|
// We directly inject file nodes into the virtual filesystem tree we built in the previous
|
|
// step, treating Magisk just like a special "module".
|
|
|
|
if get_magisk_tmp() != "/sbin" || get_path_env().split(":").all(|s| s != "/sbin") {
|
|
inject_magisk_bins(&mut system);
|
|
}
|
|
if !zygisk_name.is_empty() {
|
|
inject_zygisk_bins(&mut system, zygisk_name);
|
|
}
|
|
|
|
// Step 3: Extract all supported read-only partition roots
|
|
//
|
|
// For simplicity and backwards compatibility on older Android versions, when constructing
|
|
// Magisk modules, we always assume that there is only a single read-only partition mounted
|
|
// at /system. However, on modern Android there are actually multiple read-only partitions
|
|
// mounted at their respective paths. We need to extract these subtrees out of the main
|
|
// tree and treat them as individual trees.
|
|
|
|
let mut roots = BTreeMap::new(); /* mapOf(partition_name -> FsNode) */
|
|
if let FsNode::Directory { children } = &mut system {
|
|
for dir in SECONDARY_READ_ONLY_PARTITIONS {
|
|
// Only treat these nodes as root iff it is actually a directory in rootdir
|
|
if let Ok(attr) = dir.get_attr()
|
|
&& attr.is_dir()
|
|
{
|
|
let name = dir.trim_start_matches('/');
|
|
if let Some(root) = children.remove(name) {
|
|
roots.insert(name, root);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
roots.insert("system", system);
|
|
|
|
// Reuse the path buffer
|
|
path.clear();
|
|
path.push_str("/");
|
|
|
|
// Build the base worker directory path
|
|
let mut tmp = cstr::buf::default()
|
|
.join_path(get_magisk_tmp())
|
|
.join_path(WORKERDIR);
|
|
|
|
let mut tracker = PathTracker::from(&mut path, &mut tmp);
|
|
for (dir, mut root) in roots {
|
|
// Step 4: Convert virtual filesystem tree into concrete operations
|
|
//
|
|
// Compare the virtual filesystem tree we constructed against the real filesystem
|
|
// structure on-device to generate a series of "operations".
|
|
// The "core" of the logic is to decide which directories need to be rebuilt in the
|
|
// tmpfs worker directory, and real sub-nodes need to be mirrored inside it.
|
|
|
|
let path = tracker.append(dir);
|
|
root.commit(path, true).log_ok();
|
|
}
|
|
}
|