Files
SukiSU-Ultra/userspace/ksud/src/magic_mount.rs
5ec1cff b1564b77a2 ksud: refine mount
- `magic_mount` now takes the tmp_path from its argument
- `.notmpfs` and `.nomount` now behave the same

skip:
ksud: cache tmp path (74ce44de21963fddc83897846eed28eca89d78aa)

Co-authored-by: shatyuka <shatyuka@qq.com>
Signed-off-by: Faris <rsuntk@yukiprjkt.my.id>
2025-10-19 18:39:57 +08:00

456 lines
16 KiB
Rust

use std::{
cmp::PartialEq,
collections::{HashMap, hash_map::Entry},
fs::{self, DirEntry, FileType, create_dir, create_dir_all, read_dir, read_link},
os::unix::fs::{FileTypeExt, symlink},
path::{Path, PathBuf},
};
use anyhow::{Context, Result, bail};
use extattr::lgetxattr;
use rustix::{
fs::{
Gid, MetadataExt, Mode, MountFlags, MountPropagationFlags, Uid, UnmountFlags, bind_mount,
chmod, chown, mount, move_mount, remount, unmount,
},
mount::mount_change,
path::Arg,
};
use crate::{
defs::{DISABLE_FILE_NAME, KSU_MOUNT_SOURCE, MODULE_DIR, SKIP_MOUNT_FILE_NAME},
magic_mount::NodeFileType::{Directory, RegularFile, Symlink, Whiteout},
restorecon::{lgetfilecon, lsetfilecon},
utils::get_work_dir,
};
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<Self> {
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
}
}
}
#[derive(Debug)]
struct Node {
name: String,
file_type: NodeFileType,
children: HashMap<String, Node>,
// the module that owned this node
module_path: Option<PathBuf>,
replace: bool,
skip: bool,
}
impl Node {
fn collect_module_files<T: AsRef<Path>>(&mut self, module_dir: T) -> Result<bool> {
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 node = match self.children.entry(name.clone()) {
Entry::Occupied(o) => Some(o.into_mut()),
Entry::Vacant(v) => Self::new_module(&name, &entry).map(|it| v.insert(it)),
};
if let Some(node) = node {
has_file |= if node.file_type == Directory {
node.collect_module_files(dir.join(&node.name))? || node.replace
} else {
true
}
}
}
Ok(has_file)
}
fn new_root<T: ToString>(name: T) -> Self {
Node {
name: name.to_string(),
file_type: Directory,
children: Default::default(),
module_path: None,
replace: false,
skip: false,
}
}
fn new_module<T: ToString>(name: T, entry: &DirEntry) -> Option<Self> {
if let Ok(metadata) = entry.metadata() {
let path = entry.path();
let file_type = if metadata.file_type().is_char_device() && metadata.rdev() == 0 {
Some(Whiteout)
} else {
NodeFileType::from_file_type(metadata.file_type())
};
if let Some(file_type) = file_type {
let mut replace = false;
if file_type == Directory
&& let Ok(v) = lgetxattr(&path, REPLACE_DIR_XATTR)
&& String::from_utf8_lossy(&v) == "y"
{
replace = true;
}
return Some(Node {
name: name.to_string(),
file_type,
children: Default::default(),
module_path: Some(path),
replace,
skip: false,
});
}
}
None
}
}
fn collect_module_files() -> Result<Option<Node>> {
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_FILE_NAME).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, require_symlink) in [
("vendor", true),
("system_ext", true),
("product", true),
("odm", false),
] {
let path_of_root = Path::new("/").join(partition);
let path_of_system = Path::new("/system").join(partition);
if path_of_root.is_dir() && (!require_symlink || 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<Src: AsRef<Path>, Dst: AsRef<Path>>(src: Src, dst: Dst) -> Result<()> {
let src_symlink = read_link(src.as_ref())?;
symlink(&src_symlink, dst.as_ref())?;
lsetfilecon(dst.as_ref(), lgetfilecon(src.as_ref())?.as_str())?;
log::debug!(
"clone symlink {} -> {}({})",
dst.as_ref().display(),
dst.as_ref().display(),
src_symlink.display()
);
Ok(())
}
fn mount_mirror<P: AsRef<Path>, WP: AsRef<Path>>(
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<P: AsRef<Path>, WP: AsRef<Path>>(
path: P,
work_dir_path: WP,
current: Node,
has_tmpfs: bool,
) -> Result<()> {
let mut current = current;
let path = path.as_ref().join(&current.name);
let work_dir_path = work_dir_path.as_ref().join(&current.name);
match current.file_type {
RegularFile => {
let target_path = if has_tmpfs {
fs::File::create(&work_dir_path)?;
&work_dir_path
} else {
&path
};
if let Some(module_path) = &current.module_path {
log::debug!(
"mount module file {} -> {}",
module_path.display(),
work_dir_path.display()
);
bind_mount(module_path, target_path).with_context(|| {
format!("mount module file {module_path:?} -> {work_dir_path:?}")
})?;
// we should use MS_REMOUNT | MS_BIND | MS_xxx to change mount flags
if let Err(e) = remount(target_path, MountFlags::RDONLY | MountFlags::BIND, "") {
log::warn!("make file {target_path:?} ro: {e:#?}");
}
} else {
bail!("cannot mount root file {}!", path.display());
}
}
Symlink => {
if let Some(module_path) = &current.module_path {
log::debug!(
"create module symlink {} -> {}",
module_path.display(),
work_dir_path.display()
);
clone_symlink(module_path, &work_dir_path).with_context(|| {
format!("create module symlink {module_path:?} -> {work_dir_path:?}")
})?;
} else {
bail!("cannot mount root symlink {}!", path.display());
}
}
Directory => {
let mut create_tmpfs = !has_tmpfs && current.replace && current.module_path.is_some();
if !has_tmpfs && !create_tmpfs {
for it in &mut current.children {
let (name, node) = it;
let real_path = path.join(name);
let need = match node.file_type {
Symlink => true,
Whiteout => real_path.exists(),
_ => {
if let Ok(metadata) = real_path.symlink_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 {
if current.module_path.is_none() {
log::error!(
"cannot create tmpfs on {}, ignore: {name}",
path.display()
);
node.skip = true;
continue;
}
create_tmpfs = true;
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) = &current.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)
.context("bind self")
.with_context(|| format!("creating tmpfs for {path:?} at {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();
let result = if let Some(node) = current.children.remove(&name) {
if node.skip {
continue;
}
do_magic_mount(&path, &work_dir_path, node, has_tmpfs)
.with_context(|| format!("magic mount {}/{name}", path.display()))
} else if has_tmpfs {
mount_mirror(&path, &work_dir_path, &entry)
.with_context(|| format!("mount mirror {}/{name}", path.display()))
} else {
Ok(())
};
if let Err(e) = result {
if has_tmpfs {
return Err(e);
} else {
log::error!("mount child {}/{name} failed: {e:#?}", path.display());
}
}
}
}
if current.replace {
if current.module_path.is_none() {
bail!(
"dir {} is declared as replaced but it is root!",
path.display()
);
} else {
log::debug!("dir {} is replaced", path.display());
}
}
for (name, node) in current.children.into_iter() {
if node.skip {
continue;
}
if let Err(e) = do_magic_mount(&path, &work_dir_path, node, has_tmpfs)
.with_context(|| format!("magic mount {}/{name}", path.display()))
{
if has_tmpfs {
return Err(e);
} else {
log::error!("mount child {}/{name} failed: {e:#?}", path.display());
}
}
}
if create_tmpfs {
log::debug!(
"moving tmpfs {} -> {}",
work_dir_path.display(),
path.display()
);
if let Err(e) = remount(&work_dir_path, MountFlags::RDONLY | MountFlags::BIND, "") {
log::warn!("make dir {path:?} ro: {e:#?}");
}
move_mount(&work_dir_path, &path)
.context("move self")
.with_context(|| format!("moving tmpfs {work_dir_path:?} -> {path:?}"))?;
// make private to reduce peer group count
if let Err(e) = mount_change(&path, MountPropagationFlags::PRIVATE) {
log::warn!("make dir {path:?} private: {e:#?}");
}
}
}
Whiteout => {
log::debug!("file {} is removed", path.display());
}
}
Ok(())
}
pub fn magic_mount(tmp_path: &String) -> Result<()> {
if let Some(root) = collect_module_files()? {
log::debug!("collected: {:#?}", root);
let tmp_dir = Path::new(tmp_path).join("workdir");
ensure_dir_exists(&tmp_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);
}
fs::remove_dir(tmp_dir).ok();
result
} else {
log::info!("no modules to mount, skipping!");
Ok(())
}
}