diff --git a/native/src/base/cstr.rs b/native/src/base/cstr.rs index 5f3a78c01..72d74641e 100644 --- a/native/src/base/cstr.rs +++ b/native/src/base/cstr.rs @@ -68,7 +68,7 @@ pub mod buf { // Trait definitions -pub trait Utf8CStrBuf: Write + AsRef + Deref { +pub trait Utf8CStrBuf: Display + Write + AsRef + Deref { // The length of the string without the terminating null character. // assert_true(len <= capacity - 1) fn len(&self) -> usize; diff --git a/native/src/base/files.rs b/native/src/base/files.rs index bc7abf188..c8bdc278b 100644 --- a/native/src/base/files.rs +++ b/native/src/base/files.rs @@ -182,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"; diff --git a/native/src/base/mount.rs b/native/src/base/mount.rs index bc330d555..aa1b0234d 100644 --- a/native/src/base/mount.rs +++ b/native/src/base/mount.rs @@ -3,13 +3,14 @@ use libc::c_ulong; use std::ptr; impl Utf8CStr { - pub fn bind_mount_to<'a>(&'a self, path: &'a Utf8CStr) -> OsResult<'a, ()> { + pub fn bind_mount_to<'a>(&'a self, path: &'a Utf8CStr, rec: bool) -> OsResult<'a, ()> { + let flag = if rec { libc::MS_REC } else { 0 }; unsafe { libc::mount( self.as_ptr(), path.as_ptr(), ptr::null(), - libc::MS_BIND, + libc::MS_BIND | flag, ptr::null(), ) .check_os_err("bind_mount", Some(self), Some(path)) diff --git a/native/src/core/lib.rs b/native/src/core/lib.rs index 6d7ff8447..a1122b294 100644 --- a/native/src/core/lib.rs +++ b/native/src/core/lib.rs @@ -14,6 +14,7 @@ use cxx::{ExternType, type_id}; use daemon::{MagiskD, daemon_entry}; use derive::Decodable; use logging::{android_logging, setup_logfile, zygisk_close_logd, zygisk_get_logd, zygisk_logging}; +use module::load_modules; use mount::{find_preinit_device, revert_unmount}; use resetprop::{persist_delete_prop, persist_get_prop, persist_get_props, persist_set_prop}; use selinux::{lgetfilecon, lsetfilecon, restorecon, setfilecon}; @@ -30,6 +31,7 @@ mod consts; mod daemon; mod db; mod logging; +mod module; mod mount; mod package; mod resetprop; @@ -146,6 +148,7 @@ pub mod ffi { value: *const c_char, serial: u32, ); + unsafe fn load_prop_file(filename: *const c_char, skip_svc: bool); } unsafe extern "C++" { @@ -220,6 +223,8 @@ pub mod ffi { #[namespace = "rust"] fn daemon_entry(); + #[namespace = "rust"] + fn load_modules(module_list: &[ModuleInfo], zygisk_name: &str); } // Default constructors @@ -282,3 +287,7 @@ pub fn get_prop(name: &Utf8CStr, persist: bool) -> String { pub fn set_prop(name: &Utf8CStr, value: &Utf8CStr, skip_svc: bool) -> bool { unsafe { ffi::set_prop_rs(name.as_ptr(), value.as_ptr(), skip_svc) == 0 } } + +pub fn load_prop_file(filename: &Utf8CStr, skip_svc: bool) { + unsafe { ffi::load_prop_file(filename.as_ptr(), skip_svc) }; +} diff --git a/native/src/core/module.cpp b/native/src/core/module.cpp index 90b6d4178..16ff711cf 100644 --- a/native/src/core/module.cpp +++ b/native/src/core/module.cpp @@ -1,358 +1,12 @@ #include #include -#include -#include -#include #include #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) { - char *b = buf + ssprintf(buf, sizeof(buf), "%s/" MODULEMNT "/%.*s/", - get_magisk_tmp(), (int) m.name.size(), m.name.data()); - - // Read props - strcpy(b, "system.prop"); - if (access(buf, F_OK) == 0) { - LOGI("%.*s: loading [system.prop]\n", (int) m.name.size(), m.name.data()); - // 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); - } - if (get_magisk_tmp() != "/sbin"sv || !str_contains(getenv("PATH") ?: "", "/sbin")) { - // Need to inject our binaries into /system/bin - inject_magisk_bins(system); - } - - if (zygisk_enabled) { - string native_bridge_orig = get_prop(NBPROP); - if (native_bridge_orig.empty()) { - native_bridge_orig = "0"; - } - native_bridge = native_bridge_orig != "0" ? ZYGISKLDR + native_bridge_orig : ZYGISKLDR; - set_prop(NBPROP, native_bridge.data()); - // Weather Huawei's Maple compiler is enabled. - // If so, system server will be created by a special Zygote which ignores the native bridge - // and make system server out of our control. Avoid it by disabling. - 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(); - } -} - /************************ * Filesystem operations ************************/ @@ -485,7 +139,22 @@ rust::Vec MagiskD::handle_modules() const noexcept { exec_module_scripts("post-fs-data", collect_modules(zygisk, false)); // Recollect modules (module scripts could remove itself) auto list = collect_modules(zygisk, true); - load_modules(zygisk, list); + + if (zygisk) { + string native_bridge_orig = get_prop(NBPROP); + if (native_bridge_orig.empty()) { + native_bridge_orig = "0"; + } + native_bridge = native_bridge_orig != "0" ? ZYGISKLDR + native_bridge_orig : ZYGISKLDR; + set_prop(NBPROP, native_bridge.data()); + // Whether Huawei's Maple compiler is enabled. + // If so, system server will be created by a special Zygote which ignores the native bridge + // and make system server out of our control. Avoid it by disabling. + if (get_prop("ro.maple.enable") == "1") { + set_prop("ro.maple.enable", "0"); + } + } + rust::load_modules(rust::Slice(list), zygisk ? native_bridge : ""); return list; } diff --git a/native/src/core/module.rs b/native/src/core/module.rs new file mode 100644 index 000000000..f357b37f0 --- /dev/null +++ b/native/src/core/module.rs @@ -0,0 +1,572 @@ +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"); + + 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(); + } +} diff --git a/native/src/core/mount.rs b/native/src/core/mount.rs index 73fb645b8..e84395ab0 100644 --- a/native/src/core/mount.rs +++ b/native/src/core/mount.rs @@ -68,7 +68,7 @@ pub fn setup_module_mount() { .join_path(MODULEMNT); let _: LoggedResult<()> = try { module_mnt.mkdir(0o755)?; - cstr!(MODULEROOT).bind_mount_to(&module_mnt)?; + cstr!(MODULEROOT).bind_mount_to(&module_mnt, false)?; module_mnt.remount_mount_point_flags(libc::MS_RDONLY)?; }; } 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(); -} diff --git a/native/src/init/rootdir.rs b/native/src/init/rootdir.rs index b2d9e30c4..9f2c4a7f8 100644 --- a/native/src/init/rootdir.rs +++ b/native/src/init/rootdir.rs @@ -82,7 +82,7 @@ impl MagiskInit { debug!("Mount [{}] -> [{}]", src, dest); clone_attr(&dest, &src)?; dest.get_secontext(&mut con)?; - src.bind_mount_to(&dest)?; + src.bind_mount_to(&dest, false)?; self.overlay_con .push(OverlayAttr(dest.to_owned(), con.to_owned())); mount_list.push_str(dest.as_str()); diff --git a/native/src/init/selinux.rs b/native/src/init/selinux.rs index d7042a43b..84514fa54 100644 --- a/native/src/init/selinux.rs +++ b/native/src/init/selinux.rs @@ -49,13 +49,13 @@ enum SePatchStrategy { fn mock_fifo(target: &Utf8CStr, mock: &Utf8CStr) -> LoggedResult<()> { debug!("Hijack [{}]", target); mock.mkfifo(0o666)?; - mock.bind_mount_to(target).log() + mock.bind_mount_to(target, false).log() } fn mock_file(target: &Utf8CStr, mock: &Utf8CStr) -> LoggedResult<()> { debug!("Hijack [{}]", target); drop(mock.create(libc::O_RDONLY, 0o666)?); - mock.bind_mount_to(target).log() + mock.bind_mount_to(target, false).log() } impl MagiskInit { diff --git a/native/src/init/twostage.rs b/native/src/init/twostage.rs index d132392e3..4677195e2 100644 --- a/native/src/init/twostage.rs +++ b/native/src/init/twostage.rs @@ -37,7 +37,7 @@ pub(crate) fn hexpatch_init_for_second_stage(writable: bool) { } let attr = src.follow_link().get_attr()?; dest.set_attr(&attr)?; - dest.bind_mount_to(src)?; + dest.bind_mount_to(src, false)?; }; } } @@ -89,12 +89,15 @@ impl MagiskInit { cstr!("/init").rename_to(cstr!("/sdcard")).log_ok(); // First try to mount magiskinit from rootfs to workaround Samsung RKP - if cstr!("/sdcard").bind_mount_to(cstr!("/sdcard")).is_ok() { + if cstr!("/sdcard") + .bind_mount_to(cstr!("/sdcard"), false) + .is_ok() + { debug!("Bind mount /sdcard -> /sdcard"); } else { // Binding mounting from rootfs is not supported before Linux 3.12 cstr!("/data/magiskinit") - .bind_mount_to(cstr!("/sdcard")) + .bind_mount_to(cstr!("/sdcard"), false) .log_ok(); debug!("Bind mount /data/magiskinit -> /sdcard"); }