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, 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 SECONDARY_READ_ONLY_PARTITIONS: [&Utf8CStr; 3] = [cstr!("/vendor"), cstr!("/product"), cstr!("/system_ext")]; type FsNodeMap = BTreeMap; 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 }, 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(), }); } 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(), }, ); } 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 } => { module_log!("mklink", path.tmp, target); path.tmp.create_symlink_to(target)?; if 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"), }, ); children.insert( "resetprop".to_string(), FsNode::Symlink { target: Utf8CString::from("./magisk"), }, ); children.insert( "supolicy".to_string(), FsNode::Symlink { target: Utf8CString::from("./magiskpolicy"), }, ); } // First find whether /system/bin exists let bin = system.children().and_then(|c| c.get_mut("bin")); if let Some(FsNode::Directory { children }) = bin { inject(children); return; } // If /system/bin node does not exist, use the first suitable directory in PATH let path_env = get_path_env(); let bin_paths = path_env.split(':').filter_map(|path| { if SECONDARY_READ_ONLY_PARTITIONS .iter() .any(|p| path.starts_with(p.as_str())) { let path = Utf8CString::from(path); if let Ok(attr) = path.get_attr() && (attr.st.st_mode & 0x0001) != 0 { return Some(path); } } None }); 'path_loop: for path in bin_paths { let components = Path::new(&path) .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(); } }