Files
Magisk/native/src/core/module.rs
2025-08-28 16:15:59 -07:00

884 lines
30 KiB
Rust

use crate::consts::{MODULEMNT, MODULEROOT, MODULEUPGRADE, WORKERDIR};
use crate::daemon::MagiskD;
use crate::ffi::{ModuleInfo, exec_module_scripts, exec_script, get_magisk_tmp};
use crate::mount::setup_module_mount;
use crate::resetprop::load_prop_file;
use base::{
DirEntry, Directory, FsPathBuilder, LibcReturn, LoggedResult, OsResultStatic, ResultExt,
SilentLogExt, Utf8CStr, Utf8CStrBuf, Utf8CString, WalkResult, clone_attr, cstr, debug, error,
info, libc, raw_cstr, warn,
};
use libc::{AT_REMOVEDIR, MS_RDONLY, O_CLOEXEC, O_CREAT, O_RDONLY};
use std::collections::BTreeMap;
use std::os::fd::{AsRawFd, IntoRawFd};
use std::path::{Component, Path};
use std::ptr;
use std::sync::atomic::Ordering;
const MAGISK_BIN_INJECT_PARTITIONS: [&Utf8CStr; 4] = [
cstr!("/system/"),
cstr!("/vendor/"),
cstr!("/product/"),
cstr!("/system_ext/"),
];
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) {
module_log!(reason, dest, src);
// Ignore any kind of error here. If a single bind mount fails due to selinux permissions or
// kernel limitations, don't let it break module mount entirely.
src.bind_mount_to(dest, rec).log_ok();
dest.remount_mount_point_flags(MS_RDONLY).log_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);
Ok(())
}
// File path 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 reborrow, 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> {
path: &'a mut dyn Utf8CStrBuf,
len: usize,
}
impl PathTracker<'_> {
fn from<'a>(path: &'a mut dyn Utf8CStrBuf) -> PathTracker<'a> {
let len = path.len();
PathTracker { path, len }
}
fn append(&mut self, name: &str) -> PathTracker<'_> {
let len = self.path.len();
self.path.append_path(name);
PathTracker {
path: self.path,
len,
}
}
fn reborrow(&mut self) -> PathTracker<'_> {
Self::from(self.path)
}
}
impl Drop for PathTracker<'_> {
// Revert back to the original state after finish using the buffer
fn drop(&mut self) {
self.path.truncate(self.len);
}
}
struct FilePaths<'a> {
real: PathTracker<'a>,
worker: PathTracker<'a>,
module_mnt: PathTracker<'a>,
module_root: PathTracker<'a>,
}
impl FilePaths<'_> {
fn append(&mut self, name: &str) -> FilePaths<'_> {
FilePaths {
real: self.real.append(name),
worker: self.worker.append(name),
module_mnt: self.module_mnt.append(name),
module_root: self.module_root.append(name),
}
}
fn reborrow(&mut self) -> FilePaths<'_> {
FilePaths {
real: self.real.reborrow(),
worker: self.worker.reborrow(),
module_mnt: self.module_mnt.reborrow(),
module_root: self.module_root.reborrow(),
}
}
fn real(&self) -> &Utf8CStr {
self.real.path
}
fn worker(&self) -> &Utf8CStr {
self.worker.path
}
fn module_mnt(&self) -> &Utf8CStr {
self.module_mnt.path
}
fn module(&self) -> &Utf8CStr {
self.module_root.path
}
}
enum FsNode {
Directory { children: FsNodeMap },
File { src: Utf8CString },
Symlink { target: Utf8CString },
MagiskLink,
Whiteout,
}
impl FsNode {
fn new_dir() -> FsNode {
FsNode::Directory {
children: BTreeMap::new(),
}
}
fn collect(&mut self, mut paths: FilePaths) -> LoggedResult<()> {
let FsNode::Directory { children } = self else {
return Ok(());
};
let mut dir = Directory::open(paths.module())?;
while let Some(entry) = dir.read()? {
let entry_paths = paths.append(entry.name());
let path = entry_paths.module();
if entry.is_dir() {
let node = children
.entry(entry.name().to_string())
.or_insert_with(FsNode::new_dir);
node.collect(entry_paths)?;
} 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;
}
}
if entry_paths.real().exists() {
clone_attr(entry_paths.real(), path)?;
}
children
.entry(entry.name().to_string())
.or_insert_with(|| FsNode::File {
// Make sure to mount from module_mnt, not module
src: entry_paths.module_mnt().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
}
}
_ => true,
}
}
fn children(&mut self) -> Option<&mut FsNodeMap> {
match self {
FsNode::Directory { children } => Some(children),
_ => None,
}
}
fn commit(&mut self, mut path: FilePaths, 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.reborrow())?;
// 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.worker(), path.real(), true);
} else {
for (name, node) in children {
let path = path.append(name);
node.commit(path, false)?;
}
}
}
FsNode::File { src } => {
bind_mount("mount", src, path.real(), false);
}
_ => {
error!("Unable to handle '{}': parent should be tmpfs", path.real());
}
}
Ok(())
}
fn commit_tmpfs(&mut self, mut path: FilePaths) -> LoggedResult<()> {
match self {
FsNode::Directory { children } => {
path.worker().mkdirs(0o000)?;
if path.real().exists() {
clone_attr(path.real(), path.worker())?;
} else if let Some(p) = path.worker().parent_dir() {
let parent = Utf8CString::from(p);
clone_attr(&parent, path.worker())?;
}
// 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);
mount_dummy(
"mount",
&src,
path.worker(),
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.worker(), entry.is_dir())?;
}
}
}
// Finally, commit children
for (name, node) in children {
let path = path.append(name);
node.commit_tmpfs(path)?;
}
}
FsNode::File { src } => {
mount_dummy("mount", src, path.worker(), false)?;
}
FsNode::Symlink { target } => {
module_log!("mklink", path.worker(), target);
path.worker().create_symlink_to(target)?;
if path.real().exists() {
clone_attr(path.real(), path.worker())?;
}
}
FsNode::MagiskLink => {
if let Some(name) = path.real().file_name()
&& name == "supolicy"
{
module_log!("mklink", path.worker(), "./magiskpolicy");
path.worker().create_symlink_to(cstr!("./magiskpolicy"))?;
} else {
module_log!("mklink", path.worker(), "./magisk");
path.worker().create_symlink_to(cstr!("./magisk"))?;
}
}
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, is_emulator: bool) {
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::MagiskLink);
children.insert("resetprop".to_string(), FsNode::MagiskLink);
children.insert("supolicy".to_string(), FsNode::MagiskLink);
}
// Strip /system prefix to insert correct node
fn strip_system_prefix(orig_item: &str) -> String {
match orig_item.strip_prefix("/system/") {
Some(rest) => format!("/{rest}"),
None => orig_item.to_string(),
}
}
let path_env = get_path_env();
let mut candidates = vec![];
for orig_item in path_env.split(':') {
// Filter non-suitable paths
if !MAGISK_BIN_INJECT_PARTITIONS
.iter()
.any(|p| orig_item.starts_with(p.as_str()))
{
continue;
}
// Flatten apex path is not suitable too
if orig_item.starts_with("/system/apex/") {
continue;
}
// We want to keep /system/xbin/su on emulators (for debugging)
if is_emulator && orig_item.starts_with("/system/xbin") {
continue;
}
// Override existing su first
let su_path = Utf8CString::from(format!("{orig_item}/su"));
if su_path.exists() {
let item = strip_system_prefix(orig_item);
candidates.push((item, 0));
break;
}
let path = Utf8CString::from(orig_item);
if let Ok(attr) = path.get_attr()
&& (attr.st.st_mode & 0x0001) != 0
&& let Ok(mut dir) = Directory::open(&path)
{
let mut count = 0;
if dir
.pre_order_walk(|e| {
if e.is_file() {
count += 1;
}
Ok(WalkResult::Continue)
})
.is_err()
{
// Skip, we cannot ensure the result is correct
continue;
}
let item = strip_system_prefix(orig_item);
candidates.push((item, count));
}
}
// Sort by amount of files
candidates.sort_by_key(|&(_, count)| count);
'path_loop: for candidate in candidates {
let components = Path::new(&candidate.0)
.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(name: &str, system: &mut FsNode) {
#[cfg(target_pointer_width = "64")]
let has_32_bit = cstr!("/system/bin/linker").exists();
#[cfg(target_pointer_width = "32")]
let has_32_bit = true;
if has_32_bit {
let lib = system
.children()
.map(|c| c.entry("lib".to_string()).or_insert_with(FsNode::new_dir));
if let Some(FsNode::Directory { children }) = lib {
let mut bin_path = cstr::buf::default().join_path(get_magisk_tmp());
#[cfg(target_pointer_width = "64")]
bin_path.append_path("magisk32");
#[cfg(target_pointer_width = "32")]
bin_path.append_path("magisk");
// There are some devices that announce ABI as 64 bit only, but ship with linker
// because they make use of a special 32 bit to 64 bit translator (such as tango).
// In this case, magisk32 does not exist, so inserting it will cause bind mount
// failure and affect module mount. Native bridge injection does not support these
// kind of translators anyway, so simply check if magisk32 exists here.
if bin_path.exists() {
children.insert(
name.to_string(),
FsNode::File {
src: bin_path.to_owned(),
},
);
}
}
}
#[cfg(target_pointer_width = "64")]
if cstr!("/system/bin/linker64").exists() {
let lib64 = system
.children()
.map(|c| c.entry("lib64".to_string()).or_insert_with(FsNode::new_dir));
if let Some(FsNode::Directory { children }) = lib64 {
let bin_path = cstr::buf::default()
.join_path(get_magisk_tmp())
.join_path("magisk");
children.insert(
name.to_string(),
FsNode::File {
src: bin_path.to_owned(),
},
);
}
}
}
fn upgrade_modules() -> LoggedResult<()> {
let mut upgrade = Directory::open(cstr!(MODULEUPGRADE)).silent()?;
let ufd = upgrade.as_raw_fd();
let root = Directory::open(cstr!(MODULEROOT))?;
while let Some(e) = upgrade.read()? {
if !e.is_dir() {
continue;
}
let module_name = e.name();
let mut disable = false;
// Cleanup old module if exists
if root.contains_path(module_name) {
let module = root.open_as_dir_at(module_name)?;
// If the old module is disabled, we need to also disable the new one
disable = module.contains_path(cstr!("disable"));
module.remove_all()?;
root.unlink_at(module_name, AT_REMOVEDIR)?;
}
info!("Upgrade / New module: {module_name}");
unsafe {
libc::renameat(
ufd,
module_name.as_ptr(),
root.as_raw_fd(),
module_name.as_ptr(),
)
.check_os_err("renameat", Some(module_name), None)?;
}
if disable {
let path = cstr::buf::default()
.join_path(module_name)
.join_path("disable");
let _ = root.open_as_file_at(&path, O_RDONLY | O_CREAT | O_CLOEXEC, 0)?;
}
}
upgrade.remove_all()?;
cstr!(MODULEUPGRADE).remove()?;
Ok(())
}
fn for_each_module(mut func: impl FnMut(&DirEntry) -> LoggedResult<()>) -> LoggedResult<()> {
let mut root = Directory::open(cstr!(MODULEROOT))?;
while let Some(ref e) = root.read()? {
if e.is_dir() && e.name() != ".core" {
func(e)?;
}
}
Ok(())
}
pub fn disable_modules() {
for_each_module(|e| {
let dir = e.open_as_dir()?;
dir.open_as_file_at(cstr!("disable"), O_RDONLY | O_CREAT | O_CLOEXEC, 0)?;
Ok(())
})
.log_ok();
}
fn run_uninstall_script(module_name: &Utf8CStr) {
let script = cstr::buf::default()
.join_path(MODULEROOT)
.join_path(module_name)
.join_path("uninstall.sh");
exec_script(&script);
}
pub fn remove_modules() {
for_each_module(|e| {
let dir = e.open_as_dir()?;
if dir.contains_path(cstr!("uninstall.sh")) {
run_uninstall_script(e.name());
}
Ok(())
})
.log_ok();
cstr!(MODULEROOT).remove_all().log_ok();
}
fn collect_modules(zygisk_enabled: bool, open_zygisk: bool) -> Vec<ModuleInfo> {
let mut modules = Vec::new();
#[allow(unused_mut)] // It's possible that z32 and z64 are unused
for_each_module(|e| {
let name = e.name();
let dir = e.open_as_dir()?;
if dir.contains_path(cstr!("remove")) {
info!("{name}: remove");
if dir.contains_path(cstr!("uninstall.sh")) {
run_uninstall_script(name);
}
dir.remove_all()?;
e.unlink()?;
return Ok(());
}
dir.unlink_at(cstr!("update"), 0).ok();
if dir.contains_path(cstr!("disable")) {
return Ok(());
}
let mut z32 = -1;
let mut z64 = -1;
let is_zygisk = dir.contains_path(cstr!("zygisk"));
if zygisk_enabled {
// Riru and its modules are not compatible with zygisk
if name == "riru-core" || dir.contains_path(cstr!("riru")) {
return Ok(());
}
fn open_fd_safe(dir: &Directory, name: &Utf8CStr) -> i32 {
dir.open_as_file_at(name, O_RDONLY | O_CLOEXEC, 0)
.log()
.map(IntoRawFd::into_raw_fd)
.unwrap_or(-1)
}
if open_zygisk && is_zygisk {
#[cfg(target_arch = "arm")]
{
z32 = open_fd_safe(&dir, cstr!("zygisk/armeabi-v7a.so"));
}
#[cfg(target_arch = "aarch64")]
{
z32 = open_fd_safe(&dir, cstr!("zygisk/armeabi-v7a.so"));
z64 = open_fd_safe(&dir, cstr!("zygisk/arm64-v8a.so"));
}
#[cfg(target_arch = "x86")]
{
z32 = open_fd_safe(&dir, cstr!("zygisk/x86.so"));
}
#[cfg(target_arch = "x86_64")]
{
z32 = open_fd_safe(&dir, cstr!("zygisk/x86.so"));
z64 = open_fd_safe(&dir, cstr!("zygisk/x86_64.so"));
}
#[cfg(target_arch = "riscv64")]
{
z64 = open_fd_safe(&dir, cstr!("zygisk/riscv64.so"));
}
dir.unlink_at(cstr!("zygisk/unloaded"), 0).ok();
}
} else {
// Ignore zygisk modules when zygisk is not enabled
if is_zygisk {
info!("{name}: ignore");
return Ok(());
}
}
modules.push(ModuleInfo {
name: name.to_string(),
z32,
z64,
});
Ok(())
})
.log_ok();
if zygisk_enabled && open_zygisk {
let mut use_memfd = true;
let mut convert_to_memfd = |fd: i32| -> i32 {
if fd < 0 {
return fd;
}
if use_memfd {
let memfd = unsafe {
libc::syscall(
libc::SYS_memfd_create,
raw_cstr!("jit-cache"),
libc::MFD_CLOEXEC,
) as i32
};
if memfd >= 0 {
unsafe {
if libc::sendfile(memfd, fd, ptr::null_mut(), i32::MAX as usize) < 0 {
libc::close(memfd);
} else {
libc::close(fd);
return memfd;
}
}
}
// Some error occurred, don't try again
use_memfd = false;
}
fd
};
modules.iter_mut().for_each(|m| {
m.z32 = convert_to_memfd(m.z32);
m.z64 = convert_to_memfd(m.z64);
});
}
modules
}
impl MagiskD {
pub fn handle_modules(&self) {
setup_module_mount();
upgrade_modules().ok();
let zygisk = self.zygisk_enabled.load(Ordering::Acquire);
let modules = collect_modules(zygisk, false);
exec_module_scripts(cstr!("post-fs-data"), &modules);
// Recollect modules (module scripts could remove itself)
let modules = collect_modules(zygisk, true);
self.apply_modules(&modules);
self.module_list.set(modules).ok();
}
fn apply_modules(&self, module_list: &[ModuleInfo]) {
let mut system = FsNode::new_dir();
// Build all the base "prefix" paths
let mut root = cstr::buf::default().join_path("/");
let mut module_dir = cstr::buf::default().join_path(MODULEROOT);
let mut module_mnt = cstr::buf::default()
.join_path(get_magisk_tmp())
.join_path(MODULEMNT);
let mut worker = cstr::buf::default()
.join_path(get_magisk_tmp())
.join_path(WORKERDIR);
// Create a collection of all relevant paths
let mut root_paths = FilePaths {
real: PathTracker::from(&mut root),
worker: PathTracker::from(&mut worker),
module_mnt: PathTracker::from(&mut module_mnt),
module_root: PathTracker::from(&mut module_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.
for info in module_list {
let mut module_paths = root_paths.append(&info.name);
{
// Read props
let prop = module_paths.append("system.prop");
if prop.module().exists() {
load_prop_file(prop.module());
}
}
{
// Check whether skip mounting
let skip = module_paths.append("skip_mount");
if skip.module().exists() {
continue;
}
}
{
// Double check whether the system folder exists
let sys = module_paths.append("system");
if sys.module().exists() {
info!("{}: loading module files", &info.name);
system.collect(sys).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, self.is_emulator);
}
// Handle zygisk
if self.zygisk_enabled.load(Ordering::Acquire) {
let mut zygisk = self.zygisk.lock().unwrap();
zygisk.set_prop();
inject_zygisk_bins(&zygisk.lib_name, &mut system);
}
// 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);
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 = root_paths.append(dir);
root.commit(path, true).log_ok();
}
}
}