#include #include #include #include #include #include #include #include #include using namespace std; #ifdef MAGISK_DEBUG #define VLOGI(tag, from, to) LOGI("%-8s: %s <- %s\n", tag, to, from) #else #define VLOGI(tag, from, to) LOGI("%-8s: %s\n", tag, to) #endif #define TYPE_MIRROR (1 << 0) /* mount from mirror */ #define TYPE_INTER (1 << 1) /* intermediate node */ #define TYPE_SKEL (1 << 2) /* replace with tmpfs */ #define TYPE_MODULE (1 << 3) /* mount from module */ #define TYPE_ROOT (1 << 4) /* partition root */ #define TYPE_CUSTOM (1 << 5) /* custom node type overrides all */ #define TYPE_DIR (TYPE_INTER|TYPE_SKEL|TYPE_ROOT) vector module_list; class node_entry; class dir_node; class inter_node; class mirror_node; class skel_node; class module_node; class root_node; static void merge_node(node_entry *a, node_entry *b); template static bool isa(node_entry *node); static int bind_mount(const char *from, const char *to) { int ret = xmount(from, to, nullptr, MS_BIND, nullptr); if (ret == 0) VLOGI("bind_mnt", from, to); return ret; } 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_MIRROR; } template<> uint8_t type_id() { return TYPE_SKEL; } template<> uint8_t type_id() { return TYPE_MODULE; } template<> uint8_t type_id() { return TYPE_ROOT; } class node_entry { public: virtual ~node_entry() = default; bool is_dir() { return file_type() == DT_DIR; } bool is_lnk() { return file_type() == DT_LNK; } bool is_reg() { return file_type() == DT_REG; } uint8_t type() { return node_type; } const string &name() { return _name; } // Paths const string &node_path(); string mirror_path() { return mirror_dir + node_path(); } dir_node *parent() { return _parent; } virtual void mount() = 0; static string module_mnt; static string mirror_dir; protected: template node_entry(const char *name, uint8_t file_type, T*) : _name(name), _file_type(file_type), node_type(type_id()) {} template node_entry(T*) : node_type(type_id()) {} void create_and_mount(const string &src); /* Use top bit of _file_type for node exist status */ bool exist() { return static_cast(_file_type & (1 << 7)); } void set_exist(bool b) { if (b) _file_type |= (1 << 7); else _file_type &= ~(1 << 7); } uint8_t file_type() { return static_cast(_file_type & ~(1 << 7)); } private: friend void merge_node(node_entry *a, node_entry *b); friend class dir_node; bool need_skel_upgrade(node_entry *child); // Node properties string _name; uint8_t _file_type; uint8_t node_type; dir_node *_parent = nullptr; // Cache string _node_path; }; class dir_node : public node_entry { public: friend void merge_node(node_entry *a, node_entry *b); typedef map map_type; typedef map_type::iterator map_iter; ~dir_node() override { for (auto &it : children) delete it.second; children.clear(); } // Return false to indicate need to upgrade to module bool collect_files(const char *module, int dfd); // Return false to indicate need to upgrade to skeleton 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 *child(string_view name) { return iter_to_node(children.find(name)); } // Lazy val root_node *root() { if (!_root) _root = _parent->root(); return _root; } // Return child with name or nullptr node_entry *extract(string_view name); // Return false if rejected bool insert(node_entry *node) { return node ? iter_to_node(insert(children.find(node->_name), node->node_type, [=](auto _) { return node; })) != nullptr : false; } // Return inserted node or null if rejected template T *emplace(string_view name, Args &...args) { return iter_to_node(insert(children.find(name), type_id(), [&](auto _) { return new T(std::forward(args)...); })); } // Return inserted node, existing node with same rank, or null template T *emplace_or_get(string_view name, Args &...args) { return iter_to_node(insert(children.find(name), type_id(), [&](auto _) { return new T(std::forward(args)...); }, true)); } // Return upgraded node or null if rejected template T *upgrade(string_view name, Args &...args) { return iter_to_node(upgrade(children.find(name), args...)); } protected: template dir_node(const char *name, uint8_t file_type, T *self) : node_entry(name, file_type, self) { if constexpr (std::is_same_v) _root = self; } template dir_node(node_entry *node, T *self) : node_entry(self) { merge_node(this, node); if constexpr (std::is_same_v) _root = self; } template dir_node(const char *name, T *self) : dir_node(name, DT_DIR, self) {} template T *iter_to_node(const map_iter &it) { return reinterpret_cast(it == children.end() ? nullptr : it->second); } /* fn signature: (node_ent *&) -> node_ent * * fn gets reference to the existing node, may be null. * If fn consumes input, need to set reference to null. * Returns new node or null to reject the insertion. */ template map_iter insert(map_iter it, uint8_t type, Func fn, bool allow_same = false); template map_iter upgrade(map_iter it, Args &...args) { return insert(it, type_id(), [&](node_entry *&ex) -> node_entry * { if (!ex) return nullptr; if constexpr (!std::is_same_v) { // Type check if specific type is selected if (!isa(ex)) return nullptr; } auto node = new To(reinterpret_cast(ex), std::forward(args)...); ex = nullptr; return node; }); } // dir nodes host children map_type children; // Root node lookup cache root_node *_root; }; class root_node : public dir_node { public: root_node(const char *name) : dir_node(name, this), prefix("") {} root_node(node_entry *node) : dir_node(node, this), prefix("/system") {} const char * const prefix; }; class inter_node : public dir_node { public: inter_node(const char *name, const char *module) : dir_node(name, this), module(module) {} private: const char *module; friend class module_node; }; class module_node : public node_entry { public: module_node(const char *module, dirent *entry) : node_entry(entry->d_name, entry->d_type, this), module(module) {} module_node(node_entry *node, const char *module) : node_entry(this), module(module) { merge_node(this, node); } module_node(inter_node *node) : module_node(node, node->module) {} void mount() override; private: const char *module; }; class mirror_node : public node_entry { public: mirror_node(dirent *entry) : node_entry(entry->d_name, entry->d_type, this) {} void mount() override { create_and_mount(mirror_path()); } }; class skel_node : public dir_node { public: skel_node(node_entry *node); void mount() override; }; // Poor man's dynamic cast without RTTI template static bool isa(node_entry *node) { return node && (node->type() & type_id()); } template static T *dyn_cast(node_entry *node) { return isa(node) ? reinterpret_cast(node) : nullptr; } // Merge b -> a, b will be deleted static void merge_node(node_entry *a, node_entry *b) { a->_name.swap(b->_name); a->_file_type = b->_file_type; a->_parent = b->_parent; // Merge children if both is dir if (auto aa = dyn_cast(a); aa) { if (auto bb = dyn_cast(b); bb) { aa->children.merge(bb->children); for (auto &pair : aa->children) pair.second->_parent = aa; } } delete b; } string node_entry::module_mnt; string node_entry::mirror_dir; const string &node_entry::node_path() { if (_parent && _node_path.empty()) _node_path = _parent->node_path() + '/' + _name; return _node_path; } /************************* * Node Tree Construction *************************/ template dir_node::map_iter dir_node::insert(dir_node::map_iter it, uint8_t type, Func fn, bool allow_same) { node_entry *node = nullptr; if (it != children.end()) { if (it->second->node_type < type) { // Upgrade existing node only if higher precedence node = fn(it->second); if (!node) return children.end(); if (it->second) merge_node(node, 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 { if (allow_same && it->second->node_type == type) return it; return children.end(); } } else { node = fn(node); if (!node) return it; node->_parent = this; it = children.emplace(node->_name, node).first; } return it; } node_entry* dir_node::extract(string_view name) { auto it = children.find(name); if (it != children.end()) { auto ret = it->second; children.erase(it); return ret; } return nullptr; } skel_node::skel_node(node_entry *node) : dir_node(node, this) { string mirror = mirror_path(); if (auto dir = open_dir(mirror.data()); dir) { set_exist(true); for (dirent *entry; (entry = xreaddir(dir.get()));) { // Insert mirror nodes emplace(entry->d_name, entry); } } else { // It is actually possible that mirror does not exist (nested mount points) // Set self to non exist so this node will be ignored at mount set_exist(false); return; } for (auto it = children.begin(); it != children.end(); ++it) { // Need to upgrade all inter_node children to skel_node if (isa(it->second)) it = upgrade(it); } } bool node_entry::need_skel_upgrade(node_entry *child) { /* We need to upgrade to skeleton if: * - Target does not exist * - Source or target is a symlink */ bool upgrade = false; struct stat st; if (lstat(child->node_path().data(), &st) != 0) { upgrade = true; } else { child->set_exist(true); if (child->is_lnk() || S_ISLNK(st.st_mode)) upgrade = true; } return upgrade; } bool dir_node::prepare() { bool to_skel = false; for (auto it = children.begin(); it != children.end();) { if (need_skel_upgrade(it->second)) { if (node_type > type_id()) { // Upgrade will fail, remove the unsupported child node delete it->second; it = children.erase(it); continue; } // Tell parent to upgrade self to skel to_skel = true; // If child is inter_node, upgrade to module if (auto nit = upgrade(it); nit != children.end()) { it = nit; goto next_node; } } if (auto dn = dyn_cast(it->second); dn && dn->is_dir() && !dn->prepare()) { // Upgrade child to skeleton it = upgrade(it); } next_node: ++it; continue; } return !to_skel; } bool dir_node::collect_files(const char *module, int dfd) { auto dir = xopen_dir(xopenat(dfd, _name.data(), O_RDONLY | O_CLOEXEC)); if (!dir) return true; for (dirent *entry; (entry = xreaddir(dir.get()));) { if (entry->d_name == ".replace"sv) { // Stop traversing and tell parent to upgrade self to module return false; } if (entry->d_type == DT_DIR) { // Need check cause emplace could fail due to previous module dir replace if (auto dn = emplace_or_get(entry->d_name, entry->d_name, module); dn && !dn->collect_files(module, dirfd(dir.get()))) { // Upgrade node to module due to '.replace' upgrade(dn->name(), module); } } else { emplace(entry->d_name, module, entry); } } return true; } /************************ * Mount Implementations ************************/ void node_entry::create_and_mount(const string &src) { const string &dest = node_path(); if (is_lnk()) { VLOGI("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(src.data(), dest.data()); } } void module_node::mount() { string src = module_mnt + module + parent()->root()->prefix + node_path(); if (exist()) clone_attr(mirror_path().data(), src.data()); if (isa(parent())) create_and_mount(src); else if (is_dir() || is_reg()) bind_mount(src.data(), node_path().data()); } void skel_node::mount() { if (!exist()) return; string src = mirror_path(); const string &dest = node_path(); file_attr a; getattr(src.data(), &a); mkdir(dest.data(), 0); if (!isa(parent())) { // We don't need another layer of tmpfs if parent is skel xmount("tmpfs", dest.data(), "tmpfs", 0, nullptr); VLOGI("mnt_tmp", "tmpfs", dest.data()); } setattr(dest.data(), &a); dir_node::mount(); } /**************** * Magisk Stuffs ****************/ class magisk_node : public node_entry { public: magisk_node(const char *name) : node_entry(name, DT_REG, this) {} void mount() override { const string &dir_name = parent()->node_path(); if (name() == "magisk") { for (int i = 0; applet_names[i]; ++i) { string dest = dir_name + "/" + applet_names[i]; VLOGI("create", "./magisk", dest.data()); xsymlink("./magisk", dest.data()); } } else { for (int i = 0; init_applet[i]; ++i) { string dest = dir_name + "/" + init_applet[i]; VLOGI("create", "./magiskinit", dest.data()); xsymlink("./magiskinit", dest.data()); } } create_and_mount(MAGISKTMP + "/" + name()); } }; static void inject_magisk_bins(root_node *system) { auto bin = system->child("bin"); if (!bin) { bin = new inter_node("bin", ""); system->insert(bin); } // Insert binaries bin->insert(new magisk_node("magisk")); bin->insert(new magisk_node("magiskinit")); // Also delete all applets to make sure no modules can override it for (int i = 0; applet_names[i]; ++i) delete bin->extract(applet_names[i]); for (int i = 0; init_applet[i]; ++i) delete bin->extract(init_applet[i]); } static void mount_modules() { node_entry::mirror_dir = MAGISKTMP + "/" MIRRDIR; node_entry::module_mnt = MAGISKTMP + "/" MODULEMNT "/"; auto root = make_unique(""); auto system = new root_node("system"); root->insert(system); char buf1[4096]; char buf2[4096]; LOGI("* Loading modules\n"); for (const auto &m : module_list) { auto module = m.data(); char *b1 = buf1 + sprintf(buf1, MODULEROOT "/%s/", module); // Read props strcpy(b1, "system.prop"); if (access(buf1, F_OK) == 0) { LOGI("%s: loading [system.prop]\n", module); load_prop_file(buf1, false); } // Copy sepolicy rules strcpy(b1, "sepolicy.rule"); char *b2 = buf2 + sprintf(buf2, "%s/" MIRRDIR "/persist", MAGISKTMP.data()); if (access(buf1, F_OK) == 0 && access(buf2, F_OK) == 0) { b2 += sprintf(b2, "/magisk/%s", module); xmkdirs(buf2, 0755); strcpy(b2, "/sepolicy.rule"); cp_afc(buf1, buf2); } // Check whether skip mounting strcpy(b1, "skip_mount"); if (access(buf1, F_OK) == 0) continue; // Double check whether the system folder exists strcpy(b1, "system"); if (access(buf1, F_OK) != 0) continue; LOGI("%s: loading mount files\n", module); b1[-1] = '\0'; int fd = xopen(buf1, O_RDONLY | O_CLOEXEC); system->collect_files(module, fd); close(fd); } if (MAGISKTMP != "/sbin") { // Need to inject our binaries into /system/bin inject_magisk_bins(system); } if (system->is_empty()) return; // 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); old) { auto new_node = new root_node(old); root->insert(new_node); } } } root->prepare(); root->mount(); } static void prepare_modules() { // Upgrade modules if (auto dir = open_dir(MODULEUPGRADE); dir) { int ufd = dirfd(dir.get()); int mfd = xopen(MODULEROOT, O_RDONLY | O_CLOEXEC); for (dirent *entry; (entry = xreaddir(dir.get()));) { if (entry->d_type == DT_DIR) { // Cleanup old module if exists if (faccessat(mfd, entry->d_name, F_OK, 0) == 0) { frm_rf(xopenat(mfd, entry->d_name, O_RDONLY | O_CLOEXEC)); unlinkat(mfd, entry->d_name, AT_REMOVEDIR); } LOGI("Upgrade / New module: %s\n", entry->d_name); renameat(ufd, entry->d_name, mfd, entry->d_name); } } close(mfd); rm_rf(MODULEUPGRADE); } // Setup module mount (workaround nosuid selabel issue) auto src = MAGISKTMP + "/" MIRRDIR MODULEROOT; auto dest = MAGISKTMP + "/" MODULEMNT; bind_mount(src.data(), dest.data()); restorecon(); chmod(SECURE_DIR, 0700); } static void collect_modules() { auto dir = xopen_dir(MODULEROOT); int dfd = dirfd(dir.get()); for (dirent *entry; (entry = xreaddir(dir.get()));) { if (entry->d_type == DT_DIR) { if (entry->d_name == ".core"sv) continue; int modfd = xopenat(dfd, entry->d_name, O_RDONLY); run_finally f([=]{ close(modfd); }); if (faccessat(modfd, "remove", F_OK, 0) == 0) { LOGI("%s: remove\n", entry->d_name); auto uninstaller = MODULEROOT + "/"s + entry->d_name + "/uninstall.sh"; if (access(uninstaller.data(), F_OK) == 0) exec_script(uninstaller.data()); frm_rf(xdup(modfd)); unlinkat(dfd, entry->d_name, AT_REMOVEDIR); continue; } unlinkat(modfd, "update", 0); if (faccessat(modfd, "disable", F_OK, 0) != 0) module_list.emplace_back(entry->d_name); } } } void handle_modules() { prepare_modules(); collect_modules(); // Execute module scripts LOGI("* Running module post-fs-data scripts\n"); exec_module_script("post-fs-data", module_list); // Recollect modules (module scripts could remove itself) module_list.clear(); collect_modules(); mount_modules(); }