use crate::defs::{KSU_MOUNT_SOURCE, MODULE_DIR, SKIP_MOUNT_FILE_NAME, TEMP_DIR}; use crate::magic_mount::NodeFileType::{Directory, RegularFile, Symlink, Whiteout}; use crate::restorecon::{lgetfilecon, lsetfilecon}; use anyhow::{bail, Context, Result}; use extattr::lgetxattr; use rustix::fs::{ bind_mount, chmod, chown, mount, move_mount, unmount, Gid, MetadataExt, Mode, MountFlags, MountPropagationFlags, Uid, UnmountFlags, }; use rustix::mount::mount_change; use rustix::path::Arg; use std::cmp::PartialEq; use std::collections::hash_map::Entry; use std::collections::HashMap; use std::fs; use std::fs::{create_dir, create_dir_all, read_dir, DirEntry, FileType}; use std::os::unix::fs::{symlink, FileTypeExt}; use std::path::{Path, PathBuf}; const REPLACE_DIR_XATTR: &str = "trusted.overlay.opaque"; #[derive(PartialEq, Eq, Hash, Clone, Debug)] enum NodeFileType { RegularFile, Directory, Symlink, Whiteout, } impl NodeFileType { fn from_file_type(file_type: FileType) -> Option { if file_type.is_file() { Some(RegularFile) } else if file_type.is_dir() { Some(Directory) } else if file_type.is_symlink() { Some(Symlink) } else { None } } } struct Node { name: String, file_type: NodeFileType, children: HashMap, // the module that owned this node module_path: Option, replace: bool, } impl Node { fn collect_module_files>(&mut self, module_dir: T) -> Result { let dir = module_dir.as_ref(); let mut has_file = false; for entry in dir.read_dir()?.flatten() { let name = entry.file_name().to_string_lossy().to_string(); let path = dir.join(&name); if let Ok(v) = lgetxattr(&path, REPLACE_DIR_XATTR) { if String::from_utf8_lossy(&v) == "y" { has_file = true; self.replace = true; } } let node = match self.children.entry(name.clone()) { Entry::Occupied(o) => Some(o.into_mut()), Entry::Vacant(v) => Self::new_module(&name, &entry, &path).map(|it| v.insert(it)), }; if let Some(node) = node { has_file |= if let Directory = node.file_type { node.collect_module_files(&dir.join(&node.name))? } else { true } } } Ok(has_file) } fn new_root(name: T) -> Self { Node { name: name.to_string(), file_type: Directory, children: Default::default(), module_path: None, replace: false, } } fn new_module>( name: T, entry: &DirEntry, module_path: P, ) -> Option { if let Ok(metadata) = entry.metadata() { let file_type = if metadata.file_type().is_char_device() && metadata.dev() == 0 && metadata.ino() == 0 { Some(Whiteout) } else { NodeFileType::from_file_type(metadata.file_type()) }; if let Some(file_type) = file_type { return Some(Node { name: name.to_string(), file_type, children: Default::default(), module_path: Some(PathBuf::from(module_path.as_ref())), replace: false, }); } } None } } fn collect_module_files() -> Result> { let mut root = Node::new_root(""); let mut system = Node::new_root("system"); let module_root = Path::new(MODULE_DIR); let mut has_file = false; for entry in module_root.read_dir()?.flatten() { if !entry.file_type()?.is_dir() { continue; } if entry.path().join("disable").exists() || entry.path().join(SKIP_MOUNT_FILE_NAME).exists() { continue; } let mod_system = entry.path().join("system"); if !mod_system.is_dir() { continue; } log::debug!("collecting {}", entry.path().display()); has_file |= system.collect_module_files(&mod_system)?; } if has_file { for partition in vec!["vendor", "system_ext", "product", "odm"] { let path_of_root = Path::new("/").join(partition); let path_of_system = Path::new("/system").join(partition); if path_of_root.is_dir() && path_of_system.is_symlink() { let name = partition.to_string(); if let Some(node) = system.children.remove(&name) { root.children.insert(name, node); } } } root.children.insert("system".to_string(), system); Ok(Some(root)) } else { Ok(None) } } fn clone_symlink, Dst: AsRef>(src: Src, dst: Dst) -> Result<()> { symlink(src.as_ref(), dst.as_ref())?; lsetfilecon(dst.as_ref(), lgetfilecon(src.as_ref())?.as_str())?; Ok(()) } fn mount_mirror, WP: AsRef>( path: P, work_dir_path: WP, entry: &DirEntry, ) -> Result<()> { let path = path.as_ref().join(entry.file_name()); let work_dir_path = work_dir_path.as_ref().join(entry.file_name()); let file_type = entry.file_type()?; if file_type.is_file() { log::debug!( "mount mirror file {} -> {}", path.display(), work_dir_path.display() ); fs::File::create(&work_dir_path)?; bind_mount(&path, &work_dir_path)?; } else if file_type.is_dir() { log::debug!( "mount mirror dir {} -> {}", path.display(), work_dir_path.display() ); create_dir(&work_dir_path)?; let metadata = entry.metadata()?; chmod(&work_dir_path, Mode::from_raw_mode(metadata.mode()))?; unsafe { chown( &work_dir_path, Some(Uid::from_raw(metadata.uid())), Some(Gid::from_raw(metadata.gid())), )?; } lsetfilecon(&work_dir_path, lgetfilecon(&path)?.as_str())?; for entry in read_dir(&path)?.flatten() { mount_mirror(&path, &work_dir_path, &entry)?; } } else if file_type.is_symlink() { log::debug!( "create mirror symlink {} -> {}", path.display(), work_dir_path.display() ); clone_symlink(&path, &work_dir_path)?; } Ok(()) } fn do_magic_mount, WP: AsRef>( path: P, work_dir_path: WP, current: Node, has_tmpfs: bool, ) -> Result<()> { let mut current = current; let path = path.as_ref().join(¤t.name); let work_dir_path = work_dir_path.as_ref().join(¤t.name); match current.file_type { RegularFile => { if has_tmpfs { fs::File::create(&work_dir_path)?; } if let Some(module_path) = ¤t.module_path { log::debug!( "mount module file {} -> {}", module_path.display(), work_dir_path.display() ); bind_mount(module_path, &work_dir_path)?; } else { bail!("cannot mount root file {}!", path.display()); } } Symlink => { if let Some(module_path) = ¤t.module_path { log::debug!( "create module symlink {} -> {}", module_path.display(), work_dir_path.display() ); clone_symlink(module_path, &work_dir_path)?; } else { bail!("cannot mount root symlink {}!", path.display()); } } Directory => { let mut create_tmpfs = false; if !has_tmpfs { for (name, node) in ¤t.children { let real_path = path.join(name); let need = match node.file_type { Symlink => true, Whiteout => real_path.exists(), _ => { if let Ok(metadata) = real_path.metadata() { let file_type = NodeFileType::from_file_type(metadata.file_type()) .unwrap_or(Whiteout); file_type != node.file_type || file_type == Symlink } else { // real path not exists true } } }; if need { create_tmpfs = need; break; } } } let has_tmpfs = has_tmpfs || create_tmpfs; if has_tmpfs { log::debug!( "creating tmpfs skeleton for {} at {}", path.display(), work_dir_path.display() ); create_dir_all(&work_dir_path)?; let (metadata, path) = if path.exists() { (path.metadata()?, &path) } else if let Some(module_path) = ¤t.module_path { (module_path.metadata()?, module_path) } else { bail!("cannot mount root dir {}!", path.display()); }; chmod(&work_dir_path, Mode::from_raw_mode(metadata.mode()))?; unsafe { chown( &work_dir_path, Some(Uid::from_raw(metadata.uid())), Some(Gid::from_raw(metadata.gid())), )?; } lsetfilecon(&work_dir_path, lgetfilecon(&path)?.as_str())?; } if create_tmpfs { log::debug!( "creating tmpfs for {} at {}", path.display(), work_dir_path.display() ); bind_mount(&work_dir_path, &work_dir_path)?; } if path.exists() && !current.replace { for entry in path.read_dir()?.flatten() { let name = entry.file_name().to_string_lossy().to_string(); if let Some(node) = current.children.remove(&name) { do_magic_mount(&path, &work_dir_path, node, has_tmpfs)?; } else if has_tmpfs { mount_mirror(&path, &work_dir_path, &entry)?; } } } if current.replace && current.module_path.is_none() { bail!( "dir {} is declared as replaced but it is root!", path.display() ); } for node in current.children.into_values() { do_magic_mount(&path, &work_dir_path, node, has_tmpfs)?; } if create_tmpfs { log::debug!( "moving tmpfs {} -> {}", work_dir_path.display(), path.display() ); move_mount(&work_dir_path, &path)?; } } Whiteout => {} } Ok(()) } pub fn magic_mount() -> Result<()> { if let Some(root) = collect_module_files()? { let tmp_dir = PathBuf::from(TEMP_DIR); mount(KSU_MOUNT_SOURCE, &tmp_dir, "tmpfs", MountFlags::empty(), "").context("mount tmp")?; mount_change(&tmp_dir, MountPropagationFlags::PRIVATE).context("make tmp private")?; let result = do_magic_mount("/", &tmp_dir, root, false); if let Err(e) = unmount(&tmp_dir, UnmountFlags::DETACH) { log::error!("failed to unmount tmp {}", e); } result } else { log::info!("no modules to mount, skipping!"); Ok(()) } }