Migrate module mounting to Rust

This commit is contained in:
topjohnwu 2025-05-08 21:00:40 -07:00
parent 8b7fb6cdde
commit 2df9fd9a8a
11 changed files with 615 additions and 675 deletions

View File

@ -68,7 +68,7 @@ pub mod buf {
// Trait definitions
pub trait Utf8CStrBuf: Write + AsRef<Utf8CStr> + Deref<Target = Utf8CStr> {
pub trait Utf8CStrBuf: Display + Write + AsRef<Utf8CStr> + Deref<Target = Utf8CStr> {
// The length of the string without the terminating null character.
// assert_true(len <= capacity - 1)
fn len(&self) -> usize;

View File

@ -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";

View File

@ -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))

View File

@ -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) };
}

View File

@ -1,358 +1,12 @@
#include <sys/mman.h>
#include <sys/syscall.h>
#include <sys/mount.h>
#include <map>
#include <utility>
#include <base.hpp>
#include <consts.hpp>
#include <core.hpp>
#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<inter_node>(entry->d_name, entry);
}
}
}
for (auto it = children.begin(); it != children.end(); ++it) {
// Upgrade resting inter_node children to tmpfs_node
if (isa<inter_node>(it->second))
it = upgrade<tmpfs_node>(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<tmpfs_node>()) {
// 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<dir_node>(it->second)) {
if (replace()) {
// Propagate skip mirror state to all children
dn->set_replace(true);
}
if (dn->prepare()) {
// Upgrade child to tmpfs
it = upgrade<tmpfs_node>(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<inter_node>(entry->d_name, entry->d_name);
} else {
node = dyn_cast<inter_node>(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<module_node>(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<tmpfs_node>(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<tmpfs_node>(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<tmpfs_node>(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<tmpfs_node>(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<inter_node>("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<inter_node>(dir);
bin = node ? node : bin->emplace<inter_node>(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<inter_node>("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<inter_node>("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<ModuleInfo> &module_list) {
node_entry::module_mnt = get_magisk_tmp() + "/"s MODULEMNT "/";
auto root = make_unique<root_node>("");
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<ModuleInfo> 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<const ModuleInfo>(list), zygisk ? native_bridge : "");
return list;
}

572
native/src/core/module.rs Normal file
View File

@ -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<String, FsNode>;
macro_rules! module_log {
($($args:tt)+) => {
debug!("{:8}: {} <- {}", $($args)+)
}
}
#[allow(unused_variables)]
fn bind_mount(reason: &str, src: &Utf8CStr, dest: &Utf8CStr, rec: bool) -> OsResultStatic<()> {
module_log!(reason, dest, src);
src.bind_mount_to(dest, rec)?;
dest.remount_mount_point_flags(MS_RDONLY)?;
Ok(())
}
fn mount_dummy(reason: &str, src: &Utf8CStr, dest: &Utf8CStr, is_dir: bool) -> OsResultStatic<()> {
if is_dir {
dest.mkdir(0o000)?;
} else {
dest.create(O_CREAT | O_RDONLY | O_CLOEXEC, 0o000)?;
}
bind_mount(reason, src, dest, false)
}
// File paths that act like a stack, popping out the last element
// automatically when out of scope. Using Rust's lifetime mechanism,
// we can ensure the buffer will never be incorrectly copied or modified.
// After calling append or clone, the mutable reference's lifetime is
// "transferred" to the returned object, and the compiler will guarantee
// that the original mutable reference can only be reused if and only if
// the newly created instance is destroyed.
struct PathTracker<'a> {
real: &'a mut dyn Utf8CStrBuf,
tmp: &'a mut dyn Utf8CStrBuf,
real_len: usize,
tmp_len: usize,
}
impl PathTracker<'_> {
fn from<'a>(real: &'a mut dyn Utf8CStrBuf, tmp: &'a mut dyn Utf8CStrBuf) -> PathTracker<'a> {
let real_len = real.len();
let tmp_len = tmp.len();
PathTracker {
real,
tmp,
real_len,
tmp_len,
}
}
fn append(&mut self, name: &str) -> PathTracker {
let real_len = self.real.len();
let tmp_len = self.tmp.len();
self.real.append_path(name);
self.tmp.append_path(name);
PathTracker {
real: self.real,
tmp: self.tmp,
real_len,
tmp_len,
}
}
fn clone(&mut self) -> PathTracker {
Self::from(self.real, self.tmp)
}
}
impl Drop for PathTracker<'_> {
// Revert back to the original state after finish using the buffer
fn drop(&mut self) {
self.real.truncate(self.real_len);
self.tmp.truncate(self.tmp_len);
}
}
enum FsNode {
Directory { children: FsNodeMap },
File { src: Utf8CString },
Symlink { target: Utf8CString },
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();
}
}

View File

@ -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)?;
};
}

View File

@ -1,318 +0,0 @@
#pragma once
#include <sys/mount.h>
#include <map>
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<class T> static bool isa(node_entry *node);
template<class T> static T *dyn_cast(node_entry *node);
template<class T> uint8_t type_id() { return TYPE_CUSTOM; }
template<> uint8_t type_id<dir_node>() { return TYPE_DIR; }
template<> uint8_t type_id<inter_node>() { return TYPE_INTER; }
template<> uint8_t type_id<tmpfs_node>() { return TYPE_TMPFS; }
template<> uint8_t type_id<module_node>() { return TYPE_MODULE; }
template<> uint8_t type_id<root_node>() { 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<class T>
node_entry(const char *name, uint8_t file_type, T*)
: _name(name), _file_type(file_type & 15), _node_type(type_id<T>()) {}
template<class T>
explicit node_entry(T*) : _file_type(0), _node_type(type_id<T>()) {}
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<bool>(_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<class T>
friend bool isa(node_entry *node);
uint8_t file_type() const { return static_cast<uint8_t>(_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<string_view, node_entry *>;
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<class T>
T *get_child(string_view name) { return iterator_to_node<T>(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<class T, class ...Args>
T *emplace(string_view name, Args &&...args) {
auto fn = [&](auto) { return new T(std::forward<Args>(args)...); };
return iterator_to_node<T>(insert(name, type_id<T>(), fn));
}
// Return upgraded node or null if rejected
template<class T, class ...Args>
T *upgrade(string_view name, Args &...args) {
return iterator_to_node<T>(upgrade<T>(children.find(name), args...));
}
protected:
template<class T>
dir_node(const char *name, T *self) : node_entry(name, DT_DIR, self) {
if constexpr (std::is_same_v<T, root_node>)
_root = self;
}
template<class T>
dir_node(dirent *entry, T *self) : node_entry(entry->d_name, entry->d_type, self) {
if constexpr (std::is_same_v<T, root_node>)
_root = self;
}
template<class T>
dir_node(node_entry *node, T *self) : node_entry(self) {
if constexpr (std::is_same_v<T, root_node>)
_root = self;
dir_node::consume(node);
}
void consume(node_entry *other) override {
if (auto o = dyn_cast<dir_node>(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<bool>(_file_type & (1 << 6)); }
void set_replace(bool b) { if (b) _file_type |= (1 << 6); else _file_type &= ~(1 << 6); }
template<class T = node_entry>
T *iterator_to_node(iterator it) {
return static_cast<T*>(it == children.end() ? nullptr : it->second);
}
template<typename Builder>
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<typename Builder>
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<class T, class ...Args>
iterator upgrade(iterator it, Args &&...args) {
return insert_at(it, type_id<T>(), [&](node_entry *&ex) -> node_entry * {
if (!ex) return nullptr;
auto node = new T(ex, std::forward<Args>(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<class T>
static bool isa(node_entry *node) {
return node && (node->_node_type & type_id<T>());
}
template<class T>
static T *dyn_cast(node_entry *node) {
return isa<T>(node) ? static_cast<T*>(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();
}

View File

@ -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());

View File

@ -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 {

View File

@ -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");
}