diff --git a/native/src/base/cstr.rs b/native/src/base/cstr.rs index 14b2de868..b8de56131 100644 --- a/native/src/base/cstr.rs +++ b/native/src/base/cstr.rs @@ -81,6 +81,9 @@ pub trait Utf8CStrBuf: // 2. Ensure len <= capacity - 1 // 3. All bytes from 0 to len is valid UTF-8 and does not contain null unsafe fn set_len(&mut self, len: usize); + + // self[len] = b'\0'; self.set_len(len) + fn resize(&mut self, len: usize); fn push_str(&mut self, s: &str) -> usize; fn push_lossy(&mut self, s: &[u8]) -> usize; // The capacity of the internal buffer. The maximum string length this buffer can contain @@ -185,6 +188,7 @@ impl StringExt for PathBuf { } } +#[derive(Eq, Ord, PartialOrd)] pub struct Utf8CString(String); impl Default for Utf8CString { @@ -236,6 +240,16 @@ impl Utf8CStrBuf for Utf8CString { self.0.as_mut_vec().set_len(len); } } + + fn resize(&mut self, len: usize) { + if len >= self.0.capacity() { + return; + } + unsafe { + *self.0.as_mut_ptr().add(len) = b'\0' as _; + self.set_len(len); + } + } fn push_str(&mut self, s: &str) -> usize { self.0.push_str(s); @@ -583,6 +597,14 @@ impl FsPathBuf { inner(self.0.deref_mut(), path.as_ref()); self } + + pub fn resize(mut self, len: usize) -> Self { + unsafe { + self.0.as_bytes_mut()[len] = b'\0'; + self.0.set_len(len) + }; + self + } pub fn join_fmt(mut self, name: T) -> Self { self.0.write_fmt(format_args!("/{}", name)).ok(); @@ -742,6 +764,14 @@ macro_rules! impl_str_buf_with_slice { unsafe fn set_len(&mut self, len: usize) { self.used = len; } + fn resize(&mut self, len: usize) { + if len < self.capacity() { + unsafe { + self.buf[len] = b'\0'; + self.set_len(len); + } + } + } #[inline(always)] fn push_str(&mut self, s: &str) -> usize { utf8_cstr_buf_append(self, s.as_bytes()) diff --git a/native/src/base/dir.rs b/native/src/base/dir.rs index fd3610d87..39397dd49 100644 --- a/native/src/base/dir.rs +++ b/native/src/base/dir.rs @@ -3,7 +3,7 @@ use crate::{ cstr, cstr_buf, errno, fd_path, fd_set_attr, FileAttr, FsPath, LibcReturn, Utf8CStr, Utf8CStrBuf, }; -use libc::{dirent, O_CLOEXEC, O_CREAT, O_RDONLY, O_TRUNC, O_WRONLY}; +use libc::{dirent, O_CLOEXEC, O_CREAT, O_DIRECTORY, O_RDONLY, O_TRUNC, O_WRONLY}; use std::ffi::CStr; use std::fs::File; use std::ops::Deref; @@ -195,6 +195,13 @@ impl Directory { } } + pub fn open_dir(&self, name: &CStr) -> io::Result { + let dirp = unsafe { + libc::fdopendir(self.open_raw_fd(name, O_RDONLY|O_DIRECTORY|O_CLOEXEC, 0)?).check_os_err()? + }; + Ok(Directory { dirp }) + } + pub fn contains_path(&self, path: &CStr) -> bool { // WARNING: Using faccessat is incorrect, because the raw linux kernel syscall // does not support the flag AT_SYMLINK_NOFOLLOW until 5.8 with faccessat2. diff --git a/native/src/base/files.rs b/native/src/base/files.rs index 693e733cc..b95eefbeb 100644 --- a/native/src/base/files.rs +++ b/native/src/base/files.rs @@ -3,10 +3,7 @@ use crate::{ Utf8CStrBuf, }; use bytemuck::{bytes_of, bytes_of_mut, Pod}; -use libc::{ - c_uint, makedev, mode_t, stat, EEXIST, ENOENT, F_OK, O_CLOEXEC, O_CREAT, O_PATH, O_RDONLY, - O_RDWR, O_TRUNC, O_WRONLY, -}; +use libc::{c_uint, makedev, mode_t, stat, EEXIST, ENOENT, F_OK, MS_BIND, MS_RDONLY, MS_REC, MS_REMOUNT, O_CLOEXEC, O_CREAT, O_PATH, O_RDONLY, O_RDWR, O_TRUNC, O_WRONLY}; use mem::MaybeUninit; use num_traits::AsPrimitive; use std::cmp::min; @@ -185,6 +182,10 @@ impl FileAttr { pub fn is_socket(&self) -> bool { self.is(libc::S_IFSOCK) } + + pub fn is_whiteout(&self) -> bool { + self.is_char_device() && self.st.st_rdev == 0 + } } const XATTR_NAME_SELINUX: &CStr = c"security.selinux"; @@ -199,7 +200,7 @@ impl FsPath { } pub fn create(&self, flags: i32, mode: mode_t) -> io::Result { - Ok(File::from(open_fd!(self, flags, mode)?)) + Ok(File::from(open_fd!(self, flags | O_CREAT, mode)?)) } pub fn exists(&self) -> bool { @@ -429,6 +430,18 @@ impl FsPath { false } } + + + + pub fn bind_mount_to(&self, path: &FsPath, ro: bool) -> io::Result<()> { + unsafe { + libc::mount(path.as_ptr(), self.as_ptr(), ptr::null(), MS_BIND | MS_REC, ptr::null()).as_os_err()?; + if ro { + libc::mount(ptr::null(), self.as_ptr(), ptr::null(), MS_REMOUNT | MS_BIND | MS_RDONLY, ptr::null()).as_os_err()?; + } + } + Ok(()) + } } impl FsPathFollow { diff --git a/native/src/base/result.rs b/native/src/base/result.rs index c717679d3..023642330 100644 --- a/native/src/base/result.rs +++ b/native/src/base/result.rs @@ -55,7 +55,7 @@ impl SilentResultExt for Option { pub trait ResultExt { fn log(self) -> LoggedResult; fn log_with_msg fmt::Result>(self, f: F) -> LoggedResult; - fn log_ok(self); + fn log_ok(self) -> Option; } // Internal C++ bridging logging routines @@ -108,14 +108,14 @@ impl> ResultExt for R { } #[cfg(not(debug_assertions))] - fn log_ok(self) { - self.log().ok(); + fn log_ok(self) -> Option { + self.log().ok() } #[track_caller] #[cfg(debug_assertions)] - fn log_ok(self) { - self.do_log(LogLevel::Error, Some(Location::caller())).ok(); + fn log_ok(self) -> Option { + self.do_log(LogLevel::Error, Some(Location::caller())).ok() } } diff --git a/native/src/core/lib.rs b/native/src/core/lib.rs index 65540ea6d..8fa703645 100644 --- a/native/src/core/lib.rs +++ b/native/src/core/lib.rs @@ -4,6 +4,7 @@ #![feature(fn_traits)] #![feature(unix_socket_ancillary_data)] #![feature(unix_socket_peek)] +#![feature(btree_set_entry)] #![allow(clippy::missing_safety_doc)] use crate::ffi::SuRequest; @@ -21,6 +22,7 @@ use std::mem::ManuallyDrop; use std::ops::DerefMut; use std::os::fd::FromRawFd; use zygisk::zygisk_should_load_module; +use module::deploy_modules; #[path = "../include/consts.rs"] mod consts; @@ -33,6 +35,7 @@ mod resetprop; mod socket; mod su; mod zygisk; +mod module; #[allow(clippy::needless_lifetimes)] #[cxx::bridge] @@ -239,6 +242,7 @@ pub mod ffi { #[Self = MagiskD] #[cxx_name = "Get"] fn get() -> &'static MagiskD; + fn deploy_modules(module_list: &Vec, zygisk_lib: &CxxString, magisk_path: &CxxString) -> bool; } unsafe extern "C++" { #[allow(dead_code)] diff --git a/native/src/core/module.cpp b/native/src/core/module.cpp index d1fe8ee82..23d5c69e0 100644 --- a/native/src/core/module.cpp +++ b/native/src/core/module.cpp @@ -9,284 +9,9 @@ #include #include -#include "node.hpp" - using namespace std; -#define VLOGD(tag, from, to) LOGD("%-8s: %s <- %s\n", tag, to, from) - -static int bind_mount(const char *reason, const char *from, const char *to) { - int ret = xmount(from, to, nullptr, MS_BIND | MS_REC, nullptr); - if (ret == 0) - VLOGD(reason, from, to); - return ret; -} - -/************************* - * Node Tree Construction - *************************/ - -tmpfs_node::tmpfs_node(node_entry *node) : dir_node(node, this) { - if (!replace()) { - if (auto dir = open_dir(node_path().data())) { - set_exist(true); - for (dirent *entry; (entry = xreaddir(dir.get()));) { - // create a dummy inter_node to upgrade later - emplace(entry->d_name, entry); - } - } - } - - for (auto it = children.begin(); it != children.end(); ++it) { - // Upgrade resting inter_node children to tmpfs_node - if (isa(it->second)) - it = upgrade(it); - } -} - -bool dir_node::prepare() { - // If direct replace or not exist, mount ourselves as tmpfs - bool upgrade_to_tmpfs = replace() || !exist(); - - for (auto it = children.begin(); it != children.end();) { - // We also need to upgrade to tmpfs node if any child: - // - Target does not exist - // - Source or target is a symlink (since we cannot bind mount symlink) or whiteout - bool cannot_mnt; - if (struct stat st{}; lstat(it->second->node_path().data(), &st) != 0) { - // if it's a whiteout, we don't care if the target doesn't exist - cannot_mnt = !it->second->is_wht(); - } else { - it->second->set_exist(true); - cannot_mnt = it->second->is_lnk() || S_ISLNK(st.st_mode) || it->second->is_wht(); - } - - if (cannot_mnt) { - if (_node_type > type_id()) { - // Upgrade will fail, remove the unsupported child node - LOGW("Unable to add: %s, skipped\n", it->second->node_path().data()); - delete it->second; - it = children.erase(it); - continue; - } - upgrade_to_tmpfs = true; - } - if (auto dn = dyn_cast(it->second)) { - if (replace()) { - // Propagate skip mirror state to all children - dn->set_replace(true); - } - if (dn->prepare()) { - // Upgrade child to tmpfs - it = upgrade(it); - } - } - ++it; - } - return upgrade_to_tmpfs; -} - -void dir_node::collect_module_files(std::string_view module, int dfd) { - auto dir = xopen_dir(xopenat(dfd, name().data(), O_RDONLY | O_CLOEXEC)); - if (!dir) - return; - - for (dirent *entry; (entry = xreaddir(dir.get()));) { - if (entry->d_name == ".replace"sv) { - set_replace(true); - continue; - } - - if (entry->d_type == DT_DIR) { - inter_node *node; - if (auto it = children.find(entry->d_name); it == children.end()) { - node = emplace(entry->d_name, entry->d_name); - } else { - node = dyn_cast(it->second); - } - if (node) { - node->collect_module_files(module, dirfd(dir.get())); - } - } else { - if (entry->d_type == DT_CHR) { - struct stat st{}; - int ret = fstatat(dirfd(dir.get()), entry->d_name, &st, AT_SYMLINK_NOFOLLOW); - if (ret == 0 && st.st_rdev == 0) { - // if the file is a whiteout, mark it as such - entry->d_type = DT_WHT; - } - } - emplace(entry->d_name, module, entry); - } - } -} - -/************************ - * Mount Implementations - ************************/ - -void node_entry::create_and_mount(const char *reason, const string &src, bool ro) { - const string dest = isa(parent()) ? worker_path() : node_path(); - if (is_lnk()) { - VLOGD("cp_link", src.data(), dest.data()); - cp_afc(src.data(), dest.data()); - } else { - if (is_dir()) - xmkdir(dest.data(), 0); - else if (is_reg()) - close(xopen(dest.data(), O_RDONLY | O_CREAT | O_CLOEXEC, 0)); - else - return; - bind_mount(reason, src.data(), dest.data()); - if (ro) { - xmount(nullptr, dest.data(), nullptr, MS_REMOUNT | MS_BIND | MS_RDONLY, nullptr); - } - } -} - -void module_node::mount() { - if (is_wht()) { - VLOGD("delete", "null", node_path().data()); - return; - } - std::string path{module.begin(), module.end()}; - path += parent()->root()->prefix; - path += node_path(); - string mnt_src = module_mnt + path; - { - string src = MODULEROOT "/" + path; - if (exist()) clone_attr(node_path().data(), src.data()); - } - if (isa(parent())) { - create_and_mount("module", mnt_src); - } else { - bind_mount("module", mnt_src.data(), node_path().data()); - } -} - -void tmpfs_node::mount() { - if (!is_dir()) { - create_and_mount("mirror", node_path()); - return; - } - if (!isa(parent())) { - auto worker_dir = worker_path(); - mkdirs(worker_dir.data(), 0); - clone_attr(exist() ? node_path().data() : parent()->node_path().data(), worker_dir.data()); - dir_node::mount(); - bind_mount(replace() ? "replace" : "move", worker_dir.data(), node_path().data()); - xmount(nullptr, node_path().data(), nullptr, MS_REMOUNT | MS_BIND | MS_RDONLY, nullptr); - } else { - const string dest = worker_path(); - // We don't need another layer of tmpfs if parent is tmpfs - mkdir(dest.data(), 0); - clone_attr(exist() ? node_path().data() : parent()->worker_path().data(), dest.data()); - dir_node::mount(); - } -} - -/**************** - * Magisk Stuffs - ****************/ - -class magisk_node : public node_entry { -public: - explicit magisk_node(const char *name) : node_entry(name, DT_REG, this) {} - explicit magisk_node(const char *name, const char *target) - : node_entry(name, DT_LNK, this), target(target) {} - - void mount() override { - if (target) { - string dest = isa(parent()) ? worker_path() : node_path(); - VLOGD("create", target, dest.data()); - xsymlink(target, dest.data()); - } else { - string src = get_magisk_tmp() + "/"s + name(); - if (access(src.data(), F_OK) == 0) - create_and_mount("magisk", src, true); - } - } - -private: - const char *target = nullptr; -}; - -class zygisk_node : public node_entry { -public: - explicit zygisk_node(const char *name, bool is64bit) - : node_entry(name, DT_REG, this), is64bit(is64bit) {} - - void mount() override { -#if defined(__LP64__) - const string src = get_magisk_tmp() + "/magisk"s + (is64bit ? "" : "32"); -#else - const string src = get_magisk_tmp() + "/magisk"s; - (void) is64bit; -#endif - if (access(src.data(), F_OK)) - return; - create_and_mount("zygisk", src, true); - } - -private: - bool is64bit; -}; - -static void inject_magisk_bins(root_node *system) { - dir_node* bin = system->get_child("bin"); - if (!bin) { - struct stat st{}; - bin = system; - for (auto &item: split(getenv("PATH"), ":")) { - item.erase(0, item.starts_with("/system/") ? 8 : 1); - auto system_path = "/system/" + item; - if (stat(system_path.data(), &st) == 0 && st.st_mode & S_IXOTH) { - for (const auto &dir: split(item, "/")) { - auto node = bin->get_child(dir); - bin = node ? node : bin->emplace(dir, dir.data()); - } - break; - } - } - } - - // Insert binaries - bin->insert(new magisk_node("magisk")); - bin->insert(new magisk_node("magiskpolicy")); - - // Also insert all applets to make sure no one can override it - for (int i = 0; applet_names[i]; ++i) - bin->insert(new magisk_node(applet_names[i], "./magisk")); - bin->insert(new magisk_node("supolicy", "./magiskpolicy")); -} - -static void inject_zygisk_libs(root_node *system) { - if (access("/system/bin/linker", F_OK) == 0) { - auto lib = system->get_child("lib"); - if (!lib) { - lib = new inter_node("lib"); - system->insert(lib); - } - lib->insert(new zygisk_node(native_bridge.data(), false)); - } - - if (access("/system/bin/linker64", F_OK) == 0) { - auto lib64 = system->get_child("lib64"); - if (!lib64) { - lib64 = new inter_node("lib64"); - system->insert(lib64); - } - lib64->insert(new zygisk_node(native_bridge.data(), true)); - } -} - static void load_modules(bool zygisk_enabled, const rust::Vec &module_list) { - node_entry::module_mnt = get_magisk_tmp() + "/"s MODULEMNT "/"; - - auto root = make_unique(""); - auto system = new root_node("system"); - root->insert(system); - char buf[4096]; LOGI("* Loading modules\n"); for (const auto &m : module_list) { @@ -300,26 +25,19 @@ static void load_modules(bool zygisk_enabled, const rust::Vec &modul // Do NOT go through property service as it could cause boot lock load_prop_file(buf, true); } - - // Check whether skip mounting - strcpy(b, "skip_mount"); - if (access(buf, F_OK) == 0) - continue; - - // Double check whether the system folder exists - strcpy(b, "system"); - if (access(buf, F_OK) != 0) - continue; - - LOGI("%.*s: loading mount files\n", (int) m.name.size(), m.name.data()); - b[-1] = '\0'; - int fd = xopen(buf, O_RDONLY | O_CLOEXEC); - system->collect_module_files({ m.name.begin(), m.name.end() }, fd); - close(fd); } + std::string magisk_path; if (get_magisk_tmp() != "/sbin"sv || !str_contains(getenv("PATH") ?: "", "/sbin")) { // Need to inject our binaries into /system/bin - inject_magisk_bins(system); + magisk_path = "/system/bin"; + for (struct stat st{}; auto &item: split(getenv("PATH"), ":")) { + item.erase(0, item.starts_with("/system/") ? 8 : 1); + auto &&system_path = "/system/"s + item; + if (stat(system_path.data(), &st) == 0 && st.st_mode & S_IXOTH) { + magisk_path = std::move(system_path); + break; + } + } } if (zygisk_enabled) { @@ -335,23 +53,9 @@ static void load_modules(bool zygisk_enabled, const rust::Vec &modul if (get_prop("ro.maple.enable") == "1") { set_prop("ro.maple.enable", "0"); } - inject_zygisk_libs(system); } - if (!system->is_empty()) { - // Handle special read-only partitions - for (const char *part : { "/vendor", "/product", "/system_ext" }) { - struct stat st{}; - if (lstat(part, &st) == 0 && S_ISDIR(st.st_mode)) { - if (auto old = system->extract(part + 1)) { - auto new_node = new root_node(old); - root->insert(new_node); - } - } - } - root->prepare(); - root->mount(); - } + deploy_modules(module_list, native_bridge, magisk_path); } /************************ diff --git a/native/src/core/module.rs b/native/src/core/module.rs new file mode 100644 index 000000000..efa05853f --- /dev/null +++ b/native/src/core/module.rs @@ -0,0 +1,548 @@ +use crate::consts::{MODULEMNT, WORKERDIR}; +use crate::ffi::{ModuleInfo, get_magisk_tmp}; +use base::{ + Directory, FsPath, FsPathBuf, LoggedResult, ResultExt, Utf8CStr, Utf8CStrBuf, Utf8CString, + WalkResult::Continue, clone_attr, cstr, debug, error, info, libc::O_RDONLY, path, warn, +}; +use cxx::CxxString; +use std::collections::{ + BTreeMap, BTreeSet, btree_map::Entry as MapEntry, btree_set::Entry as SetEntry, +}; + +enum Node { + Dir { + children: BTreeMap, + exist: bool, + replace: bool, + }, + File { + src: Option, + is_link: bool, + }, +} + +impl Node { + fn mount(&mut self, dest: &Utf8CStr) -> LoggedResult<()> { + if let Node::Dir { exist, .. } = self { + *exist = true; + } + self.do_mount(dest, false) + } + + fn do_mount(&self, path: &Utf8CStr, parent_tmpfs: bool) -> LoggedResult<()> { + // todo: do mount + match &self { + Node::Dir { + children, + exist, + replace, + } => { + let mut worker = if !*exist || *replace { + let mut worker = get_magisk_tmp().to_owned(); + worker.push_str("/"); + worker.push_str(WORKERDIR); + let len = worker.len(); + worker.push_str(path); + if *replace { + debug!("replace : {}", worker); + } else if parent_tmpfs { + debug!("mkdir : {}", worker); + } else { + debug!("tmpfs : {}", worker); + } + FsPath::from(&worker).mkdirs(0o000)?; + let mut buf = path.to_owned(); + while !FsPath::from(&buf).exists() { + let mut tmp = Utf8CString::default(); + FsPath::from(&buf).parent(&mut tmp); + buf = tmp.to_owned(); + } + clone_attr(FsPath::from(&buf), FsPath::from(&worker)).ok(); + worker.resize(len); + Some(worker) + } else { + None + }; + + let mut buf = path.to_owned(); + let len = buf.len(); + for (name, child) in children { + buf.push_str("/"); + buf.push_str(name); + child.do_mount(&buf, worker.is_some())?; + buf.resize(len); + } + if !*replace + && let Some(worker) = &mut worker + && let Ok(mut dest) = Directory::open(&buf) + { + let len = worker.len(); + dest.pre_order_walk(|f| { + if let Ok(name) = Utf8CStr::from_cstr(f.name()) + && !children.contains_key(&name.to_owned()) + { + f.path(&mut buf)?; + worker.push_str(&buf); + if f.is_dir() { + debug!("mkdir : {}", &worker); + let worker = FsPath::from(worker); + worker.mkdir(0o00)?; + clone_attr(FsPath::from(&buf), worker)?; + } else if f.is_symlink() { + debug!("cp_link : {} <- {}", &worker, buf); + let attr = f.get_attr()?; + let mut link = Utf8CString::default(); + f.read_link(&mut link)?; + let worker = FsPath::from(worker); + FsPath::from(&link).symlink_to(worker)?; + worker.set_attr(&attr)?; + } else { + debug!("mirror : {} <- {}", &worker, buf); + let worker = FsPath::from(worker); + worker.create(O_RDONLY, 0o000)?; + worker.bind_mount_to(FsPath::from(&buf), true)?; + } + worker.resize(len); + }; + Ok(Continue) + })?; + } + if !parent_tmpfs && let Some(mut worker) = worker { + worker.push_str(path); + debug!("move : {} <- {}", path, worker); + FsPath::from(path).bind_mount_to(FsPath::from(&worker), true)?; + } + } + Node::File { src, is_link } => match (src, is_link) { + (None, _) => { + debug!("delete : {}", path); + } + (Some(src), false) => { + if parent_tmpfs { + let mut worker = get_magisk_tmp().to_owned(); + worker.push_str("/"); + worker.push_str(WORKERDIR); + worker.push_str(path); + debug!("module : {} <- {}", worker, src); + let worker = FsPath::from(&worker); + worker.create(O_RDONLY, 0o000)?; + worker.bind_mount_to(FsPath::from(src), true)?; + } else { + debug!("module : {} <- {}", path, src); + let src = FsPath::from(src); + let path = FsPath::from(path); + clone_attr(path, src).ok(); + path.bind_mount_to(src, true)?; + } + } + (Some(src), true) => { + if parent_tmpfs { + let mut worker = get_magisk_tmp().to_owned(); + worker.push_str("/"); + worker.push_str(WORKERDIR); + worker.push_str(path); + debug!("symlink : {} <- {}", worker, src); + FsPath::from(src).symlink_to(FsPath::from(&worker))?; + } else { + unreachable!() + } + } + }, + } + Ok(()) + } + + fn extract(&mut self, path: &Utf8CStr) -> Option { + let path = path.to_owned(); + match self { + Node::Dir { children, .. } => children.remove_entry(&path).map(|(_, v)| v), + Node::File { .. } => None, + } + } +} + +enum ModuleEntry { + Dir { + name: Utf8CString, + iter: ModuleIterator, + }, + File { + name: Utf8CString, + path: Option, + is_link: bool, + }, +} + +enum CustomModule { + Dir { + children: BTreeMap, + }, + File { + path: Utf8CString, + is_link: bool, + }, +} + +trait CustomModuleExt { + fn insert_entry(&mut self, path: &Utf8CStr, target: &Utf8CStr, is_link: bool); +} + +impl CustomModuleExt for BTreeMap { + fn insert_entry(&mut self, path: &Utf8CStr, target: &Utf8CStr, is_link: bool) { + match path.split_once('/') { + Some((dir, rest)) => { + if dir.is_empty() { + return self.insert_entry(&Utf8CString::from(rest.to_owned()), target, is_link); + } + match self + .entry(Utf8CString::from(dir.to_owned())) + .or_insert_with(|| CustomModule::Dir { + children: BTreeMap::new(), + }) { + CustomModule::Dir { children } => { + children.insert_entry(&Utf8CString::from(rest.to_owned()), target, is_link); + } + CustomModule::File { .. } => { + warn!("Duplicate entry: {}", dir); + } + } + } + _ => match self.entry(path.to_owned()) { + MapEntry::Vacant(v) => { + v.insert(CustomModule::File { + path: target.to_owned(), + is_link, + }); + } + _ => { + warn!("Duplicate entry: {}", path); + } + }, + } + } +} + +trait DirExt { + fn open_dir(&mut self, name: &Utf8CStr) -> Self; +} + +impl DirExt for Vec { + fn open_dir(&mut self, name: &Utf8CStr) -> Self { + self.iter() + .filter_map(|d| d.open_dir(name.as_cstr()).ok()) + .collect() + } +} + +impl DirExt for Vec { + fn open_dir(&mut self, name: &Utf8CStr) -> Self { + let name = name.to_owned(); + self.iter_mut() + .filter_map(|d| { + if let CustomModule::Dir { children } = d { + children.remove(&name) + } else { + None + } + }) + .collect() + } +} + +struct ModuleIterator { + modules: Vec, + customs: Vec, + collected: BTreeSet, +} + +impl Iterator for ModuleIterator { + type Item = ModuleEntry; + + fn next(&mut self) -> Option { + while let Some(e) = self.modules.last_mut().and_then(|d| d.read().log_ok()) { + let res: Option = try { + let e = e?; + let name = Utf8CStr::from_cstr(e.name()).log_ok()?; + if let SetEntry::Vacant(v) = self.collected.entry(name.to_owned()) { + let mut path = Utf8CString::default(); + let attr = e.get_attr().log_ok()?; + if attr.is_symlink() { + e.read_link(&mut path).log_ok()?; + } else { + e.path(&mut path).log_ok()?; + } + let entry = if e.is_dir() { + ModuleEntry::Dir { + name: name.to_owned(), + iter: ModuleIterator { + modules: self.modules.open_dir(v.get()), + customs: self.customs.open_dir(v.get()), + collected: BTreeSet::new(), + }, + } + } else { + ModuleEntry::File { + name: name.to_owned(), + path: Some(path).take_if(|_| !attr.is_whiteout()), + is_link: attr.is_symlink(), + } + }; + v.insert(); + entry + } else if !e.is_dir() { + warn!("Duplicate entry: {}", name); + None? + } else { + None? + } + }; + + if res.is_some() { + return res; + } + + self.modules.pop(); + } + + while let Some(CustomModule::Dir { children }) = self.customs.last_mut() { + let res: Option = try { + let e = children.first_entry()?; + + let name: &Utf8CStr = e.key(); + + if let SetEntry::Vacant(v) = self.collected.entry(name.to_owned()) { + let entry = match e.get() { + CustomModule::Dir { .. } => ModuleEntry::Dir { + name: name.to_owned(), + iter: ModuleIterator { + modules: vec![], + customs: self.customs.open_dir(v.get()), + collected: BTreeSet::new(), + }, + }, + CustomModule::File { .. } => { + if let (name, CustomModule::File { path, is_link }) = e.remove_entry() { + ModuleEntry::File { + name: name.to_owned(), + path: Some(path), + is_link, + } + } else { + unreachable!() + } + } + }; + v.insert(); + entry + } else if matches!(e.get(), CustomModule::File { .. }) { + error!("Duplicate entry: {}", name); + None? + } else { + None? + } + }; + + if res.is_some() { + return res; + } + + self.customs.pop(); + } + + None + } +} + +struct ModuleList { + modules: Vec, + customs: Vec, +} + +impl ModuleList { + fn from_module_infos(module_infos: &[ModuleInfo]) -> Self { + let mut path = FsPathBuf::default().join(get_magisk_tmp()).join(MODULEMNT); + + let len = path.len(); + let mut modules = Vec::new(); + for info in module_infos { + path = path.join(&info.name); + if let Ok(m) = Directory::open(&path) { + if !m.contains_path(c"skip_mount") && m.contains_path(c"system") { + info!("{}: loading mount files", info.name); + modules.push(m); + } + } + path = path.resize(len); + } + Self { + modules, + customs: Vec::new(), + } + } + + fn inject_zygisk_bins(&mut self, zygisk_lib: &str) { + self.customs.push(CustomModule::Dir { + children: { + let mut children = BTreeMap::new(); + let mut path = FsPathBuf::default(); + #[cfg(target_pointer_width = "64")] + { + if path!("/system/bin/linker").exists() { + path = path.join("/system/lib").join(zygisk_lib); + children.insert_entry( + &path, + &FsPathBuf::default().join(get_magisk_tmp()).join("magisk32"), + false, + ); + } + if path!("/system/bin/linker64").exists() { + path = path.join("/system/lib64").join(zygisk_lib); + children.insert_entry( + &path, + &FsPathBuf::default().join(get_magisk_tmp()).join("magisk"), + false, + ); + } + } + #[cfg(target_pointer_width = "32")] + { + path = path.join("/system/lib").join(zygisk_lib); + if path!("/system/bin/linker").exists() { + children.insert_entry( + &path, + &FsPathBuf::default().join(get_magisk_tmp()).join("magisk"), + false, + ); + } + } + children + }, + }); + } + + fn inject_magisk_bins(&mut self, magisk_path: &str) { + let mut path = Utf8CString::default(); + path.push_str(magisk_path); + path.push_str("/"); + let len = path.len(); + self.customs.push(CustomModule::Dir { + children: { + let mut children = BTreeMap::new(); + + path.push_str("magisk"); + children.insert_entry( + &path, + &FsPathBuf::default().join(get_magisk_tmp()).join("magisk"), + false, + ); + path.resize(len); + path.push_str("magiskpolicy"); + children.insert_entry( + &path, + &FsPathBuf::default() + .join(get_magisk_tmp()) + .join("magiskpolicy"), + false, + ); + path.resize(len); + path.push_str("su"); + children.insert_entry(&path, cstr!("./magisk"), true); + path.resize(len); + path.push_str("resetprop"); + children.insert_entry(&path, cstr!("./magisk"), true); + path.resize(len); + path.push_str("supolicy"); + children.insert_entry(&path, cstr!("./magiskpolicy"), true); + + children + }, + }) + } + + fn iter(&mut self, name: &Utf8CStr) -> ModuleIterator { + ModuleIterator { + modules: self.modules.open_dir(name), + customs: self.customs.open_dir(name), + collected: BTreeSet::new(), + } + } +} + +pub fn deploy_modules( + module_list: &[ModuleInfo], + zygisk_lib: &CxxString, + magisk_path: &CxxString, +) -> bool { + let res: LoggedResult<()> = try { + let mut modules = ModuleList::from_module_infos(module_list); + if !magisk_path.is_empty() { + modules.inject_magisk_bins(&magisk_path.to_string_lossy()); + } + + if zygisk_lib != "0" { + modules.inject_zygisk_bins(&zygisk_lib.to_string_lossy()); + } + + let mut system_node = tree_module(path!("/system"), &mut modules.iter(cstr!("system")))?; + for dir in [cstr!("product"), cstr!("vendor"), cstr!("system_ext")] { + system_node.extract(dir).and_then(|mut n| { + let mut dest = cstr!("/").to_owned(); + dest.push_str(dir); + n.mount(&dest).log_ok() + }); + } + system_node.mount(cstr!("/system"))?; + }; + res.log().is_ok() +} + +fn tree_module(base: &FsPath, dir: &mut ModuleIterator) -> LoggedResult { + let mut children = BTreeMap::new(); + let mut target_path = FsPathBuf::default().join(base); + let mut replace = false; + let mut exist = target_path.exists(); + + debug!("tree on {}", target_path); + + let parent_len = target_path.len(); + for e in dir { + match e { + ModuleEntry::Dir { name, mut iter } => { + target_path = target_path.join(&name); + exist = exist && target_path.exists(); + children.insert(name, tree_module(&target_path, &mut iter)?); + } + ModuleEntry::File { + name, + path, + is_link, + } => { + if &name == ".replace" { + replace = true; + continue; + } + target_path = target_path.join(&name); + + if !matches!( + ( + is_link, + &path, + target_path.get_attr().map(|attr| attr.is_symlink()) + ), + (false, Some(_), Ok(false)) + ) { + exist = false; + } + + children.insert(name, Node::File { src: path, is_link }); + } + } + target_path = target_path.resize(parent_len); + } + + Ok(Node::Dir { + children, + exist, + replace, + }) +} diff --git a/native/src/core/node.hpp b/native/src/core/node.hpp deleted file mode 100644 index 744df261c..000000000 --- a/native/src/core/node.hpp +++ /dev/null @@ -1,318 +0,0 @@ -#pragma once - -#include -#include - -using namespace std; - -#define TYPE_INTER (1 << 0) /* intermediate node */ -#define TYPE_TMPFS (1 << 1) /* replace with tmpfs */ -#define TYPE_MODULE (1 << 2) /* mount from module */ -#define TYPE_ROOT (1 << 3) /* partition root */ -#define TYPE_CUSTOM (1 << 4) /* custom node type overrides all */ -#define TYPE_DIR (TYPE_INTER|TYPE_TMPFS|TYPE_ROOT) - -class node_entry; -class dir_node; -class inter_node; -class tmpfs_node; -class module_node; -class root_node; - -// Poor man's dynamic cast without RTTI -template static bool isa(node_entry *node); -template static T *dyn_cast(node_entry *node); -template uint8_t type_id() { return TYPE_CUSTOM; } -template<> uint8_t type_id() { return TYPE_DIR; } -template<> uint8_t type_id() { return TYPE_INTER; } -template<> uint8_t type_id() { return TYPE_TMPFS; } -template<> uint8_t type_id() { return TYPE_MODULE; } -template<> uint8_t type_id() { return TYPE_ROOT; } - -class node_entry { -public: - virtual ~node_entry() = default; - - // Node info - bool is_dir() const { return file_type() == DT_DIR; } - bool is_lnk() const { return file_type() == DT_LNK; } - bool is_reg() const { return file_type() == DT_REG; } - bool is_wht() const { return file_type() == DT_WHT; } - const string &name() const { return _name; } - dir_node *parent() const { return _parent; } - - // Don't call the following two functions before prepare - const string &node_path(); - const string worker_path(); - - virtual void mount() = 0; - - inline static string module_mnt; - -protected: - template - node_entry(const char *name, uint8_t file_type, T*) - : _name(name), _file_type(file_type & 15), _node_type(type_id()) {} - - template - explicit node_entry(T*) : _file_type(0), _node_type(type_id()) {} - - virtual void consume(node_entry *other) { - _name.swap(other->_name); - _file_type = other->_file_type; - _parent = other->_parent; - delete other; - } - - void create_and_mount(const char *reason, const string &src, bool ro=false); - - // Use bit 7 of _file_type for exist status - bool exist() const { return static_cast(_file_type & (1 << 7)); } - void set_exist(bool b) { if (b) _file_type |= (1 << 7); else _file_type &= ~(1 << 7); } - -private: - friend class dir_node; - - template - friend bool isa(node_entry *node); - - uint8_t file_type() const { return static_cast(_file_type & 15); } - - // Node properties - string _name; - dir_node *_parent = nullptr; - - // Cache, it should only be used within prepare - string _node_path; - - uint8_t _file_type; - const uint8_t _node_type; -}; - -class dir_node : public node_entry { -public: - using map_type = map; - using iterator = map_type::iterator; - - ~dir_node() override { - for (auto &it : children) - delete it.second; - children.clear(); - } - - /************** - * Entrypoints - **************/ - - // Traverse through module directories to generate a tree of module files - void collect_module_files(std::string_view module, int dfd); - - // Traverse through the real filesystem and prepare the tree for magic mount. - // Return true to indicate that this node needs to be upgraded to tmpfs_node. - bool prepare(); - - // Default directory mount logic - void mount() override { - for (auto &pair : children) - pair.second->mount(); - } - - /*************** - * Tree Methods - ***************/ - - bool is_empty() { return children.empty(); } - - template - T *get_child(string_view name) { return iterator_to_node(children.find(name)); } - - root_node *root() { - if (!_root) - _root = _parent->root(); - return _root; - } - - // Return child with name or nullptr - node_entry *extract(string_view name) { - auto it = children.find(name); - if (it != children.end()) { - auto ret = it->second; - children.erase(it); - return ret; - } - return nullptr; - } - - // Return false if rejected - bool insert(node_entry *node) { - auto fn = [=](auto) { return node; }; - return node && iterator_to_node(insert(node->_name, node->_node_type, fn)); - } - - // Return inserted node or null if rejected - template - T *emplace(string_view name, Args &&...args) { - auto fn = [&](auto) { return new T(std::forward(args)...); }; - return iterator_to_node(insert(name, type_id(), fn)); - } - - // Return upgraded node or null if rejected - template - T *upgrade(string_view name, Args &...args) { - return iterator_to_node(upgrade(children.find(name), args...)); - } - -protected: - template - dir_node(const char *name, T *self) : node_entry(name, DT_DIR, self) { - if constexpr (std::is_same_v) - _root = self; - } - - template - dir_node(dirent *entry, T *self) : node_entry(entry->d_name, entry->d_type, self) { - if constexpr (std::is_same_v) - _root = self; - } - - template - dir_node(node_entry *node, T *self) : node_entry(self) { - if constexpr (std::is_same_v) - _root = self; - dir_node::consume(node); - } - - void consume(node_entry *other) override { - if (auto o = dyn_cast(other)) { - children.merge(o->children); - for (auto &pair : children) - pair.second->_parent = this; - } - node_entry::consume(other); - } - - // Use bit 6 of _file_type - // Skip binding mirror for this directory - bool replace() const { return static_cast(_file_type & (1 << 6)); } - void set_replace(bool b) { if (b) _file_type |= (1 << 6); else _file_type &= ~(1 << 6); } - - template - T *iterator_to_node(iterator it) { - return static_cast(it == children.end() ? nullptr : it->second); - } - - template - iterator insert(string_view name, uint8_t type, const Builder &builder) { - return insert_at(children.find(name), type, builder); - } - - // Emplace insert a new node, or upgrade if the requested type has a higher rank. - // Return iterator to the new/upgraded node, or end() if insertion is rejected. - // fn is the node builder function. Signature: (node_entry *&) -> node_entry * - // fn gets a reference to the existing node pointer and returns a new node object. - // Input is null when there is no existing node. If returns null, the insertion is rejected. - // If fn consumes the input, it should set the reference to null. - template - iterator insert_at(iterator it, uint8_t type, const Builder &builder) { - node_entry *node = nullptr; - if (it != children.end()) { - // Upgrade existing node only if higher rank - if (it->second->_node_type < type) { - node = builder(it->second); - if (!node) - return children.end(); - if (it->second) - node->consume(it->second); - it = children.erase(it); - // Minor optimization to make insert O(1) by using hint - if (it == children.begin()) - it = children.emplace(node->_name, node).first; - else - it = children.emplace_hint(--it, node->_name, node); - } else { - return children.end(); - } - } else { - node = builder(node); - if (!node) - return children.end(); - node->_parent = this; - it = children.emplace(node->_name, node).first; - } - return it; - } - - template - iterator upgrade(iterator it, Args &&...args) { - return insert_at(it, type_id(), [&](node_entry *&ex) -> node_entry * { - if (!ex) return nullptr; - auto node = new T(ex, std::forward(args)...); - ex = nullptr; - return node; - }); - } - - // dir nodes host children - map_type children; - -private: - // Root node lookup cache - root_node *_root = nullptr; -}; - -class root_node : public dir_node { -public: - explicit root_node(const char *name) : dir_node(name, this), prefix("") { - set_exist(true); - } - explicit root_node(node_entry *node) : dir_node(node, this), prefix("/system") { - set_exist(true); - } - const char * const prefix; -}; - -class inter_node : public dir_node { -public: - inter_node(const char *name) : dir_node(name, this) {} - inter_node(dirent *entry) : dir_node(entry, this) {} -}; - -class module_node : public node_entry { -public: - module_node(std::string_view module, dirent *entry) - : node_entry(entry->d_name, entry->d_type, this), module(module) {} - - module_node(node_entry *node, std::string_view module) : node_entry(this), module(module) { - node_entry::consume(node); - } - - void mount() override; -private: - std::string_view module; -}; - -// Don't create tmpfs_node before prepare -class tmpfs_node : public dir_node { -public: - explicit tmpfs_node(node_entry *node); - void mount() override; -}; - -template -static bool isa(node_entry *node) { - return node && (node->_node_type & type_id()); -} -template -static T *dyn_cast(node_entry *node) { - return isa(node) ? static_cast(node) : nullptr; -} - -const string &node_entry::node_path() { - if (_parent && _node_path.empty()) - _node_path = _parent->node_path() + '/' + _name; - return _node_path; -} - -const string node_entry::worker_path() { - return get_magisk_tmp() + "/"s WORKERDIR + node_path(); -}