Files
Magisk/native/src/core/module.rs

580 lines
20 KiB
Rust

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");
// There are some devices that announce ABI as 64 bit only, but ship with linker64
// 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(),
},
);
}
}
}
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();
}
}