- `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>
456 lines
16 KiB
Rust
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(¤t.name);
|
|
let work_dir_path = work_dir_path.as_ref().join(¤t.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) = ¤t.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) = ¤t.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) = ¤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)
|
|
.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(())
|
|
}
|
|
}
|